集册 Redis 3.0 中文版教程 从入门到精通(中)

从入门到精通(中)

黑派客     最近更新时间:2020-08-04 05:37:59

386

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 值来表明列表中没有元素了。

列表的通用场景(Common use cases)

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

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

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

流行的 Twitter 社交网络,使用 Redis 列表来存储用户最新的微博 (tweets)。

为了一步一步的描述通用场景,假设你想加速展现照片共享社交网络主页的最近发布的图片列表。

每次用户提交一张新的照片,我们使用 LPUSH 将其 ID 添加到列表。

当用户访问主页时,我们使用 LRANGE 0 9 获取最新的 10 张照片。

上限列表(Capped)

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

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)

列表有一个特别的特性使得其适合实现队列,通常作为进程间通信系统的积木:阻塞操作。

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

  • 生产者调用 LPUSH 添加项到列表中。
  • 消费者调用 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。

还有更多你需要知道的关于列表和阻塞选项,建议你阅读下面的页面:

  • 使用 RPOLPUSH 构建更安全的队列和旋转队列。
  • 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 哈希看起来正如你期待的那样:

> 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"  

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

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 集合 (Sets)

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

> sadd myset 1 2 3  
(integer) 3  
> smembers myset  
1. 3  
2. 1  
3. 2  

我们向集合总添加了 3 个元素,然后告诉 Redis 返回所有元素。如你所见,他们没有排序,Redis 在每次调用时按随意顺序返回元素,因为没有与用户有任何元素排序协议。

我们有测试成员关系的命令。一个指定的元素存在吗?

> sismember myset 3  
(integer) 1  
> sismember myset 30  
(integer) 0  

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

集合适用于表达对象间关系。例如,我们可以很容易的实现标签。对这个问题的最简单建模,就是有一个为每个需要标记的对象的集合。集合中保存着与对象相关的标记的 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 命令实现这个,也就是对不同的集合执行交集。我们只需要:

展开阅读全文