Redis列表(list)
为了解释Redis列表我们首先需要一点理论知识,由于List常常被信息技术领域人误用,例如:"Python Lists"并不是从命名中推测的链表结构, 而是数组结构 (同样的数据结构在Ruby中叫做Array)。
从通用的角度看Redis列表只是一组有序元素。例如:10,20,1,2,3 就构成了一个列表。但是用数组结构实现的列表和用链表结构实现的列表性质上有很大不同。
Redis列表使用链表结构实现。这意味着即便已经保存了百万个元素,添加一个元素到列表头和列表尾部也只需要常数时间开销。也就是说,使用LPUSH 命令添加一个元素到一个有10个元素列表的头部和将其添加到一个有1000万元素的列表头部速度是一样的。
这样有什么弊端吗?在数组实现的列表中,根据下标查找元素的操作速度极快,而在链表实现的列表中比较慢 (这个操作与查询的下标位置偏移成正比)。
Redis 列表选择链表结构的原因是,对于数据库系统来说,能快速将元素添加到一个长列表中是至关重要的。另一个优势是Redis 列表支持在常数时间内获取固定数量元素。
当快速获取大集合中间的元素很重要时,可以使用另一个数据结构排序集合(sorted sets)。此数据结构稍后会介绍。
初步使用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 值表明列表中不再有其他元素可以弹出。
列表的常见使用场景
列表非常适合记录任务,两个典型的应用如下:
- 记住用户在社交网络上发布的最新更新。
- 用生产者消费者模型进行进程间通信,生产者将任务放入列表,消费者(通常是工作者)获取任务并执行。Redis支持特别的列表操作,使这个用例更加可靠和高效。
例如:两个著名的Ruby类库 resque 和sidekiq 使用Redis列表作为背后存储用于实现后台任务。
著名的Twitter社交网络从用户推送到Redis 列表中的信息获取最近的tweets 。
假设您的主页显示了共享网络中发布的最新照片,并且希望加速照片访问。
- 每次用户上传照片,使用LPUSH将ID加入一个Redis列表。.
- 当用户访问主页,我们使用
LRANGE 0 9
命令获取最近10条发布内容。
限制列表
在许多应用场景下我们需要存储最近的内容。 有可能是社交网络更新,日志或其他任何信息。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) 复杂度命令,从Redis列表头或尾部获取一小段范围的元素只需要常数时间。
列表阻塞操作
Redis列表支持一些特殊特性可以方便的实现队列场景, 一般用于实现系统进程间通信的阻塞操作。
假设你希望使用一个进程将任务推送到列表并使用另一个进程来实际处理任务。类似生产者/消费者模型,可以通过以下简单方式实现:
但是,有时列表可能是空的,所以RPOP只返回空值。这种情况下,消费者需要等待一段时间再用RPOP重试。这种轮询操作有几个缺点:
- 强制Redis和客户端处理无命令(当列表为空时,所有请求都不会执行实际任务,它们只返回空值)。
- 因为在工作进程收到空值之后会等待一段时间,导致产生处理延时。
为了解决此问题,Redis实现了BRPOP和BLPOP 命令,他们是RPOP和LPOP命令在空队列时的阻塞版本。这两个命令只在新元素入队列后,或指定等待时间超时情况下,才会返回。阻塞等待操作可以使用0作为超时值,此时将永久等待元素。也可以指定同时等待多个列表,并在任何列表收到元素时得到通知。
这是一个任务处理进程的BRPOP示例:
> brpop tasks 5
1) "tasks"
2) "do_something"
它的意思是:“等待列表中的元素任务
,但如果5秒后没有元素可用,则返回”。
关于 BRPOP有几点需要注意:
- 客户端遵循公平等待原则:第一个阻塞的客户端首先获得通知。
- 返回值格式与rpop不同:返回值是一个两个两元素组成的数组,因为brpop和blpop支持等待来自多个列表的元素,因此返回内容包含列表名称。
- 如果超时,则返回空值。
关于列表和阻塞操作,如需了解更多,建议阅读以下内容:
- 使用RPOPLPUSH构建更安全的队列或旋转队列。
- 该命令还有一个阻塞版本,称为BRPOPLPUSH。
Redis键的自动创建和删除
到目前为止,我们看到示例中,Redis在放入元素前无需创建空列表。同时元素清空时也无需删除空列表。Redis会自动处理列表的创建与删除。这种特性不止限于列表,在Redis内部的其他数据结构上同样适用。例如:Streams, Sets, Sorted Sets and Hashes.
这种特性可以总结如下:
- 当向集合数据类型添加元素时,如果Redis键不存在,则在添加元素之前创建一个空的集合数据类型。
- 当从集合数据类型中删除元素时,如果集合为空,则键将自动销毁。流数据类型是此规则的唯一例外。
- 调用只读命令例如:LLEN (返回列表的长度),或删除集合对应Redis键时,返回值与存在一个对应的空集合效果一样。
规则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)