阿里资深专家聊Redis 6系列(04)——数据类型详解(中)

2014年,利用工作之余,我翻译了Redis 3非稳定版的官方文档,在网络上被大量转载、推荐和盗链。6年时光白驹过隙,Redis 6稳定版已经发布,增加了很多新特性,鉴于各种资料参差不齐,或陈旧或残缺或错误,于是抽空再倒腾下。


Redis列表(Lists)

为了解释列表类型,最好先开始来点理论,因为列表这个术语在信息技术领域常常使用不当。例如,”Python Lists”,并不是字面意思(链表),实际是表示数组(和Ruby中的Array是同一种类型)。

通常列表表示有序元素的序列:10,20,1,2,3是一个列表。但是数组实现的列表和链表实现的列表,他们的属性非常不同。

Redis的列表是使用链表来实现的。这意味着,即使你的列表中有上百万个元素,增加一个元素到列表的头部或者尾部的操作都是在常量时间内完成的。使用LPUSH命令增加一个新元素到一个拥有10个元素的列表的头部的速度,与增加到一个拥有1000万个元素的列表的头部是一样的。

缺点又是什么呢?使用索引下标来访问一个数组实现的列表非常快(常量时间),但是访问链表实现的列表就没那么快了(与元素索引下标成正比的大量工作)。

Redis采用链表来实现列表是因为,对于数据库系统来说,快速插入一个元素到一个很长的列表非至关重要。另外一个强大的优势,你稍后将看到,Redis列表能在恒定时间内以恒定长度获取。

如果需要快速访问一个拥有大量元素的集合的中间数据,可以用另一个称为有序集合的数据结构。稍后将会介绍有序集合。

 

Redis列表小试牛刀

LPUSH命令从左边(头部)添加一个元素到列表,RPUSH命令从右边(尾部)添加一个元素到列表。LRANGE命令从列表中提取一个范围内的元素。

> rpush mylist A

(integer) 1

> rpush mylist B

(integer) 2

> lpush mylist first

(integer) 3

> lrange mylist 0 -1

1) "first"

2) "A"

3) "B"

注意LRANGE命令使用两个索引下标,分别是返回的范围的开始和结束元素。两个索引坐标可以是负数,表示从后往前数,所以-1表示最后一个元素,-2表示倒数第二个元素,等等。

如你所见,RPUSH添加元素到列表的右边,LPUSH添加元素到列表的左边。

两个命令都是可变参数命令,也就是说,你可以在一个命令调用中自由的添加多个元素到列表中:

> rpush mylist 1 2 3 4 5 "foo bar"

(integer) 9

> lrange mylist 0 -1

1) "first"

2) "A"

3) "B"

4) "1"

5) "2"

6) "3"

7) "4"

8) "5"

9) "foo bar"

定义在Redis列表上的一个重要操作是弹出元素。弹出元素指的是从列表中检索元素,并同时将其从列表中清除的操作。你可以从左边或者右边弹出元素,类似于你可以从列表的两端添加元素。

> rpush mylist a b c

(integer) 3

> rpop mylist

"c"

> rpop mylist

"b"

> rpop mylist

"a"

我们添加了三个元素并且又弹出了三个元素,所以这一串命令执行完以后列表是空的,没有元素可以弹出了。如果我们试图再弹出一个元素,就会得到如下结果:

> rpop mylist

(nil)

Redis返回一个NULL值来表明列表中没有元素了。

 

列表的常用场景

列表可以完成很多任务,两个有代表性的场景如下:

  1. 记录社交网络中用户最近提交的更新。
  2. 进程间使用生产者-消费者模式来进行通信,生产者添加项(item)到列表,消费者(通常是worker)消费项并执行任务。Redis有专门的列表命令来更加可靠和高效地解决这种问题。

例如,两种流行的Ruby库resque和sidekiq,都是使用Redis列表作为钩子,来实现后台作业。

流行的Twitter社交网络,将用户发表的最新推文(tweets)存储到Redis列表。

为了一步一步的描述常用场景,假设你的个人主页上要展示你上传到图片分享社交网络上最新的照片,并且想加速访问。

  1. 每次用户提交一张新的照片,我们使用LPUSH将其ID添加到列表。
  2. 当用户访问主页时,我们使用LRANGE 0 9获取最新的10张照片。

 

上限列表(Capped lists)

很多时候我们只是想用列表存储最近的项,随便这些项是什么:社交网络更新、日志或者任何其他东西。

Redis允许使用列表作为一个上限集合,使用LTRIM命令,仅仅只记住最新的N项,丢弃掉所有老的项。

LTRIM命令类似于LRANGE,但是不同于展示指定范围的元素,而是将其作为列表新值存储。所有范围外的元素都将被删除。

举个例子你就更清楚了:

> rpush mylist 1 2 3 4 5

(integer) 5

> ltrim mylist 0 2

OK

> lrange mylist 0 -1

1) "1"

2) "2"

3) "3"

上面LTRIM命令告诉Redis仅仅保存第0到2个元素,其它的都被抛弃掉。这可以让你实现一个简单而又有用的模式,一个添加操作和一个修剪操作一起,实现新增一个元素并抛弃超出的元素。

LPUSH mylist <some element>

LTRIM mylist 0 999

上面的组合命令先增加一个元素到列表中,同时只持有最新的1000个元素。使用LRANGE命令你可以访问前几个元素而不用记录非常旧的数据。

注意:尽管技术上讲LRANGE是一个O(N)时间复杂度的命令,但访问列表头尾附近的小范围是一个恒定时间的操作。

 

列表上的阻塞操作(Blocking operations)

列表有一个特别的特性,使它们适合实现队列,并且通常作为进程间通信系统的构建块:阻塞操作。

假设你想用一个进程往列表中添加项,用另一个进程来处理这些项。这就是通常的生产者-消费者模式,可以使用以下简单方式实现:

  1. 生产者调用LPUSH添加项到列表中。
  2. 消费者调用RPOP从列表提取/处理项。

然而,有时候列表是空的,没有需要处理的项,RPOP就返回NULL。所以消费者被强制等待一段时间并重试RPOP命令。这称为轮询(polling),由于其具有一些缺点,所以不合适在这种情况下:

1.强制Redis和客户端处理无用的命令(当列表为空时的所有请求都没有执行实际的工作,只会返回NULL)。

2.由于工作者收到一个NULL后会等待一段时间,这会延迟对项的处理。

于是Redis实现了BRPOP和BLPOP两个命令,它们是当列表为空时RPOP和LPOP的阻塞版本:仅当一个新元素被添加到列表时,或者到达了用户的指定超时时间,才返回给调用者。

这个是我们在工作者中调用BRPOP的例子:

> brpop tasks 5

1) "tasks"

2) "do_something"

上面的意思是:”等待tasks列表中的元素,如果5秒后还没有可用元素就返回”。

注意,你可以使用0作为超时让其一直等待元素,你也可以指定多个列表而不仅仅只是一个,同时等待多个列表,当第一个列表收到元素后就能得到通知。

关于BRPOP的一些注意事项:

1.按顺序为客户端提供服务:第一个被阻塞并等待列表的客户端,将第一个收到其他客户端添加的元素,诸如此类。

2.与RPOP的返回值不同:返回的是一个数组,其中包括键的名字,因为BRPOP和BLPOP可以阻塞等待多个列表的元素。

3.如果超时时间到达,返回NULL。

还有更多你需要知道的关于列表和阻塞的选项,建议你学习下列内容:

  1. 使用LMOVE构建更安全的队列或旋转队列(rotating queue) RPOPLPUSH。
  2. 该命令还有一个阻塞变种,称为BLMOVE。BRPOPLPUSH

(LMOVE和BLMOVE命令将取代RPOPLPUSH和BRPOPLPUSH命令,请查看命令页以了解可靠队列和循环队列两种模式,作者注)

 

键的自动创建和删除

到目前为止的例子中,我们还没有在添加元素前创建一个空的列表,也没有删除一个没有元素的空列表。要注意,当列表为空时Redis将删除该键,当向一个不存在的列表键(如使用LPUSH)添加一个元素时,将创建一个空的列表。

这并不只是针对列表,适用于所有Redis多元素组成的数据类型,因此适用于集合、有序集合和哈希。

基本上,我们可以概括为三条规则:

1.当我们向聚合(aggregate)数据类型添加一个元素时,如果目标键不存在,添加元素前将创建一个空的聚合数据类型。

2.当我们从聚合数据类型删除一个元素时,如果值会为空,则键也会被销毁。

3.在一个空键上调用一个诸如LLEN这样的只读命令(返回列表的长度),或者一个删除元素的写命令,总是会产生与操作一个持有命令期待的类型相同的空聚合类型的键一样的结果。

规则1的例子:

> del mylist

(integer) 1

> lpush mylist 1 2 3

(integer) 3

然而,如果键存在,我们不能对一个错误的类型执行操作:

> set foo bar

OK

> lpush foo 1 2 3

(error) WRONGTYPE Operation against a key holding the wrong kind of value

> type foo

string

 

规则2的例子:

> lpush mylist 1 2 3

(integer) 3

> exists mylist

(integer) 1

> lpop mylist

"3"

> lpop mylist

"2"

> lpop mylist

"1"

> exists mylist

(integer) 0

当所有元素弹出后,键就不存在了。

 

规则3的例子:

> del mylist

(integer) 0

> llen mylist

(integer) 0

> lpop mylist

(nil)

 

Redis哈希/散列(Hashes)

Redis哈希看起来正如你所期待的那样,由字段-值对(fields-values pairs)组成:

> hmset user:1000 username antirez birthyear 1977 verified 1

OK

> hget user:1000 username

"antirez"

> hget user:1000 birthyear

"1977"

> hgetall user:1000

1) "username"

2) "antirez"

3) "birthyear"

4) "1977"

5) "verified"

6) "1"

哈希就是字段-值对的集合。由于哈希容易表示对象,事实上,哈希中的字段的数量并没有限制,所以你可以在你的应用程序中以不同的方式来使用哈希。

HMSET命令为哈希设置多个字段,HGET检索一个单独的字段。HMGET类似于HGET,但是返回一个值的数组:

> hmget user:1000 username birthyear no-such-field

1) "antirez"

2) "1977"

3) (nil)

也有一些命令可以针对单个字段执行操作,例如HINCRBY:

> hincrby user:1000 birthyear 10

(integer) 1987

> hincrby user:1000 birthyear 10

(integer) 1997

你可以从命令页找到全部哈希命令的列表。

值得注意的是,小的哈希(例如,少量元素且包含比较小的值)在内存中以一种特殊的方式编码以高效利用内存(Redis实现的内部数据结构,另文再描述,作者注)。

 

Redis集合(Sets)

Redis集合是无序的字符串的集合(collections)。SADD命令添加元素到集合。还可以对集合执行很多其他的操作,例如,测试元素是否存在、对多个集合执行交集、并集和差集,等等。

> sadd myset 1 2 3

(integer) 3

> smembers myset

1. 3

2. 1

3. 2

这里我们向集合中添加了3个元素,然后告诉Redis返回所有元素。如你所见,它们没有被排序,Redis在每次调用时按任意顺序返回元素,因为其与用户之间没有关于元素排序的协议。

Redis有测试成员关系的命令。例如,检查一个元素是否存在:

> sismember myset 3

(integer) 1

> sismember myset 30

(integer) 0

“3”是集合中的成员,”30”则不是。

集合适用于表达对象间关系。例如,我们可以很容易的实现标签(tag)。对这个问题的最简单建模,就是为每个我们想要打标签的对象建立一个集合。集合中保存着与该对象相关联的标签ID。

一个例子是为新闻文章打标签。如果文章ID为1000的新闻被标签1,2,5和77所标记,一个集合就可以将这些标签ID和新闻项目相关联:

> sadd news:1000:tags 1 2 5 77

(integer) 4

我们可能还想知道其反向的关系:被给定标签所标记的所有新闻的列表:

> sadd tag:1:news 1000

(integer) 1

> sadd tag:2:news 1000

(integer) 1

> sadd tag:5:news 1000

(integer) 1

> sadd tag:77:news 1000

(integer) 1

获取指定对象的全部标签很简单:

> smembers news:1000:tags

1. 5

2. 1

3. 77

4. 2

注意:在这个例子中,我们假设你有另外一个数据结构,例如,一个Redis哈希,存储了标签ID到标签名的映射。

还有一些只要使用了正确的Redis命令就很容实现的操作。例如,我们想获取所有被标签1,2,10和27同时标记的对象的列表。我们可以使用SINTER命令实现这个,也就是对不同的集合执行交集。我们只需要:

> sinter tag:1:news tag:2:news tag:10:news tag:27:news

... results here ...

除了交集操作,你还可以执行并集、差集、随机抽取元素操作等等。

抽取一个元素的命令是SPOP,它方便为很多问题建模。例如,为了实现一个基于web的扑克游戏,你可以将你的一副牌表示为集合。假设我们使用一个字符前缀来表示(C)lubs梅花, (D)iamonds方块,(H)earts红心,(S)pades黑桃。

>  sadd deck C1 C2 C3 C4 C5 C6 C7 C8 C9 C10 CJ CQ CK

   D1 D2 D3 D4 D5 D6 D7 D8 D9 D10 DJ DQ DK H1 H2 H3

   H4 H5 H6 H7 H8 H9 H10 HJ HQ HK S1 S2 S3 S4 S5 S6

   S7 S8 S9 S10 SJ SQ SK

   (integer) 52

现在我们想为每位选手提供5张牌。SPOP命令会删除一个随机元素,并返回给客户端,这个场景下,这是一个完美操作。

然而,如果我们直接对这副牌调用这个命令,游戏的下一局中我们需要再填充一副牌,这个可能不太理想。所以一开始,我们要拷贝deck键中存储的集合到game:1:deck键中。

这是通过使用SUNIONSTORE命令完成的,这个命令通常对多个集合执行并集,然后把结果存储在另一个集合中。而对单个集合求并集就是其自身,于是我可以这样拷贝我的这副牌:

> sunionstore game:1:deck deck

(integer) 52

现在我们准备好为第一个选手提供5张牌:

> spop game:1:deck

"C6"

> spop game:1:deck

"CQ"

> spop game:1:deck

"D1"

> spop game:1:deck

"CJ"

> spop game:1:deck

"SJ"

只有一对jack,不太给力……

是时候介绍提供集合中元素数量的命令了(很像是《超级飞侠》中的台词,作者注)。这个在集合理论中称为集合的基数(cardinality,也称为集合的势,作者注),所以相应的Redis命令称为SCARD。

> scard game:1:deck

(integer) 47

数学运算为:52 - 5 = 47。

当你只需要获得随机元素而不需要从集合中删除,SRANDMEMBER命令则适合你这个任务。它具有返回重复的和非重复的元素的能力。


牛仔很忙,毕业于华中科技大学,硕士研究生,校招加入腾讯,从事电子商务相关研发工作。连续两段创业经历后,最近一份经历,是阿里巴巴国际化中台深圳团队负责人,从事阿里电商中台架构、团队管理、双十一大促、稳定性等工作。

我的人生理想是,白天当一名中学老师,晚上当一名滴滴司机。

欢迎关注微信公众号:程序员阮威

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值