Redis 基础数据结构

351 阅读8分钟

commandsredis.io/commands

前言

Redis 所有的数据结构都是以唯一的 key 字符串作为名称,然后通过这个唯一 key 值来获取相应的 value 数据。不同类型的数据结构的差异就在于 value 的结构不一样。

Redis 有 5 种基础数据结构,分别为:

  • string (字符串)
  • list (列表)
  • set (集合)
  • hash (哈希)
  • zset (有序集合)

string 字符串

string 是 Redis 最基本的数据结构。字符串结构使用非常广泛,一个常见的用途就是缓存用户信息。程序中将用户信息结构体使用 JSON 序列化成字符串,然后将序列化后的字符串塞进 Redis 来缓存。同样,取用户信息会经过一次反序列化的过程。

string 类型是二进制安全的,意思是 redis 的 string 可以包含任何数据。如数字,字符串,jpg图片或者序列化的对象。

Redis 的字符串是 动态字符串,是可以修改的字符串,内部结构实现上类似于 Java 的 ArrayList,采用 预分配冗余空间 的方式来减少内存的频繁分配。内部为当前字符串实际分配的空间 capacity 一般要高于实际字符串长度 len。当字符串长度小于 1M 时, 扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间。需要注意的是 字符串最大长度为 512M。

命令

redis> SET mykey "Hello"
"OK"
redis> GET mykey
"Hello"
redis> SET anotherkey "will expire in a minute" EX 60  #60s过期
"OK"
redis>

list 列表

Redis 的 list 相当于 Java 语言里面的 LinkedList,注意它是 链表 而不是数组。这意味着 list 的插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为 O(n)。Redis 的 list 类型其实就是一个 每个子元素都是 string 类型的双向链表。 当 list 弹出了最后一个元素之后,该数据结构自动被删除,内存被回收。

Redis 的 list 的主要功能是 pushpop、获取一个 范围的所有值 等等,这使得 list 既可以用作 ,也可以用作 队列。操作中 key 理解为链表的名字,可以添加一个元素到列表的头部(左边)或者尾部(右边)。

Redis 的 list 结构常用来做 异步队列 使用。将需要延后处理的任务结构体序列化成字符 串塞进 Redis 的列表,另一个线程从这个列表中轮询数据进行处理。

listpop 操作还有阻塞版本的,当 [lr]pop 一个 list 对象时,如果 list 是空, 或者不存在,会立即返回 nil。但是阻塞版本的 b[lr]pop 则可以阻塞,当然可以加超时时间,超时后也会返回 nil

为什么要阻塞版本的 pop 呢?主要是为了避免轮询。举个简单的例子:如果用 list 来实现一个工作队列,执行任务的 thread 可以调用阻塞版本的 pop 去获取任务,这样就可以避免轮询去检查是否有任务存在。当 list 中有任务的时候工作线程可以立即返回, 也可以避免轮询带来的延迟。

慢查询操作

慎用。lindex 相当于 Java 链表的 get(int index) 方法,它需要对链表进行遍历,时间复杂度为 O(n),性能随着参数 index 增大而变差。

命令

右进左出:队列

redis> RPUSH mylist "hello" "world"
(integer)2
redis> LLEN mylist
(integer)2
redis> LRANGE mylist 0 -1
1) "hello"
2) "world"
redis> LPOP mylist
"hello"
redis> LPOP mylist
"world"
redis> LPOP mylist
(nil)

右进右出:栈

redis> RPUSH mylist "hello" "world"
(integer) 2
redis> RPOP mylist
"world"
redis> RPOP mylist
"hello"
redis> LPOP mylist
(nil)

hash 字典

Redis 的 hash 是一个 string 类型的 field(域)value(值) 的映射表,相当于 Java 语言里面的 HashMap,它是 无序字典。所以 Redis 的 hash 是指 键值对 中的值本身又是一个键值对结构,形如value=[{field1,value1},...{fieldN,valueN}]

它的添加、删除操作的时间复杂度都是 O(1)(平均)。hash 特别适合用于存储对象。相较于将对象的每个字段存成单个 string 类型,将一个 对象 存储在 hash 类型中会占用更少的内存,并且可以更方便的存取整个对象。当 hash 移除了最后一个元素之后,该数据结构自动被删除,内存被回收。

Redis 中每个 hash 可以存储 2^32 - 1 键值对(40多亿)

hash 结构也可以用来存储用户信息,不同于字符串一次性需要全部序列化整个对象, hash 可以对用户结构中的每个字段单独存储。这样当需要获取用户信息时可以进行部分获取。而以整个字符串的形式去保存用户信息的话就只能一次性全部读取,这样就会比较浪费网络流量。 不过需要注意一点,hash 结构的存储消耗要高于单个字符串。

命令

redis> HSET myhash field1 "Hello"
(integer) 1
redis> HSET myhash field2 "World"
(integer) 1
redis> HGETALL myhash # key 和 value 间隔出现
1) "field1"
2) "hello"
3) "field2"
4) "world"
redis> HLEN myhash
(integer) 2
redis> HGET myhash field1
"hello"
redis> HSET myhash field1 "redis" # 更新操作,返回 0
(integer) 0  

set 集合

Redis 的 set 相当于 Java 语言里面的 HashSet,它内部的 键值对是无序的唯一的。当集合中最后一个元素移除之后,数据结构自动删除,内存被回收。操作中 key 理解为集合的名字,set 的操作有添加删除元素,有对多个 set 求交并差等操作。 set 是通过 hashtable 实现的,所以添加、删除和查找的复杂度都是 O(1)

集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)

set 结构可以用来存储活动中奖的用户 ID,因为有去重功能,可以保证同一个用户不会中奖两次。

命令

redis> sadd myset "hello"
(integer) 1
redis> sadd myset "hello" # 重复
(integer) 0
redis> sadd myset "world" "and" "redis"
(integer) 3
redis> smembers myset # 注意顺序,和插入的并不一致,因为 set 是无序的
1) "redis"
2) "and"
3) "hello"
4) "world"
redis> sismember myset "hello" # 查询某个 value 是否存在,相当于 contains(o)
(integer) 1
redis> scard myset # 获取长度相当于 size()
(integer) 4
redis> spop myset # 弹出一个
"world"

zset 有序列表

zset 类似于 Java 的 SortedSetHashMap 的结合体,一方面它是一个 set,保证了内部 value唯一性,另一方面它可以给每个 value 赋予一个 score,代表这个 value排序权重zset 中最后一个 value 被移除后,数据结构自动删除,内存被回收。

Redis zsetset 一样也是 string 类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个 double 类型的分数。redis 正是通过分数来为集合中的成员进行从小到大的排序。有序集合的成员是唯一的,但分数( score )却可以重复。

集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。 集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)。

zset 可以用来存粉丝列表,value 值是粉丝的用户ID,score 是关注时间,对粉丝列表按关注时间进行排序。 zset 还可以用来存储学生的成绩,value 值是学生的ID,score 是他的考试成绩。对成绩按分数进行排序就可以得到他的名次。

命令

redis> zadd myzset 9.0 "java"
(integer) 1
redis> zadd myzset 8.5 "C++"
(integer) 1
redis> zadd myzset 8.8 "C"
(integer) 1
redis> zrange myzset 0 -1 #按 score 排序列出,参数区间为排名范围
1) "C++"
2) "C"
3) "java"
redis> zrevrange myzset 0 -1 # 按 score 逆序列出,参数区间为排名范围
1) "java"
2) "C"
3) "C++"
redis> zcard myzset # 相当于 size()
(integer) 3
redis> zscore myzset "C++" # 获取指定 value 的 score
"8.5"
redis> zrank myzset "java" # 排名
(integer) 2
redis> zrangebyscore myzset 0 8.9  # 根据分值区间遍历 zset
1) "C++"
2) "C"
redis> zrangebyscore myzset -inf 8.9 withscores   # 根据分值区间 (-∞, 8.91] 遍历 zset,同时返 回分值。inf 代表 infinite,无穷大的意思。
1) "C++"
2) "8.5"
3) "C"
4) "8.8000000000000007"  # 内部 score 使用 double 类型进行存储,所以存在小数点精度问题
redis> zrem myzset "C++" # 删除 value
(integer) 1
redis> zrange myzset 0 -1
1) "C"
2) "java"

总结

list/set/hash/zset 这四种数据结构是容器型数据结构,它们共享下面两条通用规则:

  1. create if not exists

如果容器不存在,那就创建一个,再进行操作。比如 rpush 操作刚开始是没有列表的, Redis 就会自动创建一个,然后再 rpush 进去新元素。

  1. drop if no elements

如果容器里元素没有了,那么立即删除元素,释放内存。这意味着 lpop 操作到最后一个元素,列表就消失了。

过期时间

Redis 所有的数据结构都可以设置过期时间,时间到了,Redis 会自动删除相应的对象。 需要注意的是过期是以 对象 为单位。比如一个 hash 结构的过期是整个 hash 对象的过期, 而不是其中的某个子 key。 还有一个需要特别注意的地方是 如果一个字符串已经设置了过期时间,然后你调用了 set 方法修改了它,它的过期时间会消失

redis> set mystring java
OK
redis> expire mystring 60
(integer) 1
redis> ttl mystring
(integer) 55
redis> set mystring redis
OK
redis> ttl mystring
(integer) -1