夯实基础:Redis 的数据结构介绍

使用 Redis 这么久,发现自己还没写过一篇有关 Redis 数据结构的文章,从构造 Redis 整个知识体系来说,显示是不完整的。故这篇文章再次让自己回归到 Redis 的五种基本数据结构,除了描述这些数据结构的特点,也介绍如何使用 Redis 命令来操作这些数据结构。
Redis 支持的数据结构包括:

  • 字符串
  • 列表
  • 集合
  • 有序集合
  • 哈希表

需要指出的是,这些数据结构不是指 Redis 内部实现所采用的数据结构,而是指 Redis 提供给用户使用的数据结构。例如,对于集合这种数据结构,其内部可以使用整数集合(intset)来实现,也可以使用哈希表(hashtable)来实现。一般情况下,对于用户来说,我们并不需要关心集合内部的实现,只需要知道集合这种数据结构的应用场景以及集合为我们提供的操作命令就可以了。
接下来,本文依次对 Redis 这些数据结构进行介绍。

字符串

字符串是 Redis 最简单的数据结构。熟悉 Memcached 的朋友对于字符串这种数据结构一定感到很亲切,因为 Memcached 也提供了这种数据结构。跟 Memcached 不同的是,Redis 除了提供字符串这种数据结构外,还提供了其他的数据结构,而 Memcached 则只提供字符串数据结构而已。
Redis 字符串使用 SDS 来实现。SDS 是 Redis 内部使用的一种数据结构,用作 Redis 字符串的表示。由于 Redis 的键都是字符串,这里我们说字符串是指 Redis 值的数据结构类型,例如客户端执行下面命令:

1
SET website leehao.me

那么,Redis 将创建一个新的键值对,其中,

  • 键是一个字符串,字符串内部使用 SDS 来实现,用来保存“website”
  • 值也是一个字符串,字符串内部也是使用 SDS 来实现,用来保存“leehao.me”

下文我们所说的数据结构也是针对键值对的值的数据结构类型来说的,且值的数据结构是什么,我们就称之什么类型的键,例如字符串键,即键值对的值的数据结构为字符串。
使用命令 SET 可以设置字符串的值,如果需要取出字符串,可以使用命令 GET。

1
GET website

输出:

leehao.me

正常情况,客户端发送一个命令,Redis 执行这个命令并将处理结果返回给客户端。如果客户端需要同时设置多个键的值,采用每次只设置一个键值对的方法显然会导致较大的往返时延。Redis 为了解决这个问题引入了 MSET 和 MGET 命令,用来支持一次同时设置或者获取多个键的值。

1
MSET website leehao.me name leo sex male

上面的命令同时设置键 website,name,sex 的值分别为 leehao.me,leo 以及 male。
MGET 则可以同时获取多个键的值:

1
MGET website name sex

MGET 的输出为一个列表,其元素为每个键的值:

1) “leehao.me”
2) “leo”
3) “male”

如果我们需要删除某个键,可以使用命令 DEL:

1
DEL name

这样,键 name 以及对应的值 leo 都会从 Redis 中删除。DEL 命令不单可以用在字符串键上,也可以用在其他类型的键上面。

列表

Redis 的列表在内部使用链表来实现,这意味着在列表的头部或尾部添加一个元素的时间复杂度都为O(1)。由于列表使用链表来实现,故列表并不支持随机存取元素,如果需要访问列表中指定位置的元素,其时间复杂度为O(n)。

可以使用 LPUSH 或者 RPUSH 命令创建一个列表,LPUSH是指在列表的头部添加一个元素,RPUSH 是指在列表的尾部添加一个元素。例如:

1
2
3
RPUSH mylist leo
RPUSH mylist lee
LPUSH mylist first

在列表还没存在时,LPUSH 或者 RPUSH 命令都会创建一个新的列表,并将第一个元素添加进去。可以使用 LRANGE 来查看列表中的元素:

1
LRANGE mylist 0 -1

输出:

1) “first”
2) “leo”
3) “lee”

LRANGE 命令的第一个参数为键,第二个参数指定列表的开始位置,第三个参数则指定列表的结束位置。上面的 LRANGE 命令的第三个参数为 -1,表明最后一个元素,故整个命令的含义是指取出列表的所有元素。
如果需要删除列表的元素,可以使用 LPOP 或者 RPOP 命令,也可以使用阻塞版本的 BLPOP 或者 BRPOP 命令。

集合

Redis 提供集合这种数据结构类型。对于集合来说,它里面的元素都是唯一的,并不存在相同的元素。使用 SADD 可以创建一个 Redis 集合:

1
SADD myset leehao leo me 1

这样就创建了一个包含4个元素的集合 myset。可以使用 SMEMBERS 来获取集合中所有的元素:

1
SMEMBERS myset

输出:

1) “1”
2) “leo”
3) “me”
4) “leehao”

如果往集合里面添加相同的元素,则这个元素并不会被添加到集合里面:

1
2
SADD myset 1 3
SMEMBERS myset

输出:

1) “me”
2) “leehao”
3) “1”
4) “3”
5) “leo”

可见,“1” 并不会再次被添加到 myset 集合当中。

使用 SPOP 可以从集合中删除一个或多个元素:

1
SPOP myset 2

上面的命令从集合中移除两个元素,且被移除的元素是随机的。

可以使用 SINTER 来获取几个集合的交集,使用 SUNIONSTORE 来获取几个集合的并集。

有序集合

有序集合可以看作集合与哈希表的混合体。有序集合里面的元素都是唯一的,这点跟上面我们提到的集合的特点很相似,但跟集合不同的是,有序集合的元素都有一个关联的浮点数值(称为元素的分数),这一点则跟哈希表相似。
有序集合之所以称为“有序”是由于集合里面的任意两个元素都可以比较大小,且不存在两个元素的大小相等。
Redis 使用以下规则来决定两个有序集合元素的大小:

  • 如果有序集合元素 A 和 B 拥有不同的分数,那么,如果 A.score > B.score,则 A > B
  • 如果有序集合元素 A 和 B 拥有相同的分数,那么,如果 A 键字符串 > B 键字符串,则 A > B

由于有序集合中不存在两个相同的元素,故有序集合中的任意两个元素都不会出现相等的情况。

使用 ZADD 来创建一个有序集合:

1
2
3
4
del leehao
ZADD leehao 1 C++
ZADD leehao 2 Python
ZADD leehao 0 C

我们先把原有的 leehao 键删除,然后构造了一个有序集合 leehao,其中,1,2,0分别为元素 C++,Python,C 的分数。
使用命令 ZRANG 可以查看有序集合的元素:

1
ZRANGE leehao 0 -1

上面的输出有序集合内所有元素,可以看到,输出的元素已经按元素的分数从小到大进行了排列:

1) “C”
2) “C++”
3) “Python”

如果需要逆序输出元素,则可以使用命令:

1
ZREVRANGE leehao 0 -1

则元素会按元素的分数逆序输出:

1) “Python”
2) “C++”
3) “C”

如果需要输出元素的分数,则可以在 ZRANGE 命令最后添加上 WITHSCORES:

1
ZRANGE leehao 0 -1 WITHSCORES

哈希表

如果我们对 Python 的字典或者 C++ 的 std::map 这些数据结构熟悉的话,那么也一定会很快掌握 Redis 的哈希表数据结构,Redis 的哈希表同样是一种映射关系的数据结构。可以使用 HSET 或者 HMSET 来创建或者设置一个哈希表:

1
hmset leehao name leo sex male location gz

上面的命令创建了一个哈希表,它的键为 leehao,该哈希表包含3个 域 - 值 对,分别为 name -> leo,sex -> male 以及 location -> gz。

可以使用 HGET 命令获取哈希表某个 域 的值:

1
hget leehao name

输出域 name 的值:

“leo”

或者使用 HGETALL 命令获取哈希表所有 域 的值:

1
hgetall leehao

输出为每个域 - 值对的列表:

1) “name”
2) “leo”
3) “sex”
4) “male”
5) “location”
6) “gz”

参考资料

  1. https://redis.io/topics/data-types-intro
  2. https://redis.io/topics/internals-sds
  3. Redis 设计与实现,黄健宏著,机械工业出版社,2015年1月