文章目录
一、 List类型简要介绍
列表类型是用来存储多个有序的字符串,如图 2-19 所示,a、b、c、d、e 五个元素从左到右组成了一个有序的列表,列表中的每个字符串称为元素(element),一个列表最多可以存储 2 32 2^{32} 232-1个元素。在 Redis 中,可以对列表两端插入(push)和弹出(pop),还可以获取指定范围的元素列表、获取指定索引下标的元素等(如图 2-19 和图 2-20 所示)。列表是一种比较灵活的数据结构,它可以充当栈和队列的角色,在实际开发上有很多应用场景。
列表类型的特点:
- 第一、列表中的元素是有序的,这意味着可以通过索引下标获取某个元素或者某个范围的元素列表,例如要获取图 2-20 的第 5 个元素,可以执行
lindex user:1:messages 4
或者倒数第 1 个元素,lindex user:1:messages -1
就可以得到元素 e。 - 第二、区分获取和删除的区别,例如图 2-20 中的 lrem 1 b 是从列表中把从左数遇到的前 1 个 b 元素删除,这个操作会导致列表的长度从 5 变成 4;但是执行 lindex 4 只会获取元素,但列表长度是不会变化的。
- 第三、列表中的元素是允许重复的,例如图 2-21 中的列表中是包含了两个 a 元素的。
二、常用命令
lpush
LPUSH
- 将一个或者多个元素从左侧放入(头插)到 list 中。
- 语法:
LPUSH key element [element ...]
- 命令有效版本:1.0.0 之后
- 时间复杂度:只插入一个元素为 O(1), 插入多个元素为 O(N), N 为插入元素个数.
- 返回值:插入后 list 的长度。
- 示例:
lpushx
LPUSHX
- 在 key 存在时,将一个或者多个元素从左侧放入(头插)到 list 中。不存在,直接返回
- 语法:
LPUSHX key element [element ...]
- 命令有效版本:2.0.0 之后
- 时间复杂度:只插入一个元素为 O(1), 插入多个元素为 O(N), N 为插入元素个数.
- 返回值:插入后 list 的长度。
- 示例:
rpush
RPUSH
- 将一个或者多个元素从右侧放入(尾插)到 list 中。
- 语法:
RPUSH key element 1 [element ...]
- 命令有效版本:1.0.0 之后
- 时间复杂度:只插入一个元素为 O(1), 插入多个元素为 O(N), N 为插入元素个数.
- 返回值:插入后 list 的长度。
- 示例:
rpushx
RPUSHX
- 在 key 存在时,将一个或者多个元素从右侧放入(尾插)到 list 中。
- 语法:
RPUSHX key element [element ...]
- 命令有效版本:2.0.0 之后
- 时间复杂度:只插入一个元素为 O(1), 插入多个元素为 O(N), N 为插入元素个数.
- 返回值:插入后 list 的长度。
- 示例:
lrange
LRANGE
- 获取从 start 到 end 区间的所有元素,左闭右闭,下标从0开始。
- 语法:
LRANGE key start stop
- 命令有效版本:1.0.0 之后
- 时间复杂度:O(N)
- 返回值:指定区间的元素。
- 示例:
lpop
LPOP
- 从 list 左侧取出元素(即头删),count表示删除的个数,不写默认头删1个元素。
- 语法:
LPOP key [count]
- 命令有效版本:1.0.0 之后
- 时间复杂度:O(1)
- 返回值:取出的元素或者 nil。
- 示例:
rpop
RPOP
- 从 list 右侧取出元素(即尾删)。
- 语法:
RPOP key
- 命令有效版本:1.0.0 之后
- 时间复杂度:O(1)
- 返回值:取出的元素或者 nil。
- 示例:
lindex
LINDEX
- 获取从左数第 index 位置的元素。
- 语法:
LINDEX key index
- 命令有效版本:1.0.0 之后
- 时间复杂度:O(N)
- 返回值:取出的元素或者 nil。
- 示例:
linsert
LINSERT
- 在特定位置插入元素。
- 语法:
LINSERT key <BEFORE | AFTER> 1 pivot element
- 命令有效版本:2.2.0 之后
- 时间复杂度:O(N)
- 返回值:插入后的 list 长度。
- 示例:
len
LLEN
- 获取 list 长度。
- 语法:
LLEN key
- 命令有效版本:1.0.0 之后
- 时间复杂度:O(1)
- 返回值:list 的长度。
- 示例:
阻塞版本命令前言
blpop 和 brpop 是 lpop 和 rpop 的阻塞版本,和对应非阻塞版本的作用基本一致,除了:
- 在列表中有元素的情况下,阻塞和非阻塞表现是一致的。但如果列表中没有元素,非阻塞版本会理
解返回 nil,但阻塞版本会根据 timeout,阻塞一段时间,期间 Redis 可以执行其他命令,但要求执
行该命令的客户端会表现为阻塞状态(如图 2-22 所示)。 - 命令中如果设置了多个键,那么会从左向右进行遍历键,一旦有一个键对应的列表中可以弹出元
素,命令立即返回。 - 如果多个客户端同时多一个键执行 pop,则最先执行命令的客户端会得到弹出的元素。
blpop
BLPOP
- LPOP 的阻塞版本。
- 语法:
BLPOP key 1 [key ...] timeout
- 命令有效版本:1.0.0 之后
- 时间复杂度:O(1)
- 返回值:取出的元素或者 nil。
- 示例:
brpop
BRPOP
- RPOP 的阻塞版本。
- 语法:
BRPOP key [key ...] timeout
- 命令有效版本:1.0.0 之后
- 时间复杂度:O(1)
- 返回值:取出的元素或者 nil。
- 示例:
lrem
- 根据参数
count
的值,移除列表中与value
相等的元素。 - 语法:
LREM key count value
- 命令有效版本:多个版本(具体取决于Redis的版本历史,但广泛支持)
- 时间复杂度:O(k),k 是被遍历的元素数量,直到找到足够的元素或者列表被遍历完。
- 返回值:被移除的元素的数量。
- 示例:
LREM key1 2 "a"
从列表key1
中移除最多 2 个等于"a"
的元素。
ltrim
- 对一个列表进行修剪(trim),只保留指定区间内的元素,不在指定区间内的元素都将被删除。
- 语法:
LTRIM key start stop
- 命令有效版本:多个版本(具体取决于Redis的版本历史,但广泛支持)
- 时间复杂度:O(k),k 是被移除的元素数量。
- 返回值:总是返回 “OK”。
- 示例:
LTRIM key1 2 4
只保留列表key1
中索引为 2、3 和 4 的元素,移除其他所有元素。
lset
- 将列表
key
中索引为index
的元素的值设置为value
。 - 语法:
LSET key index value
- 命令有效版本:多个版本(具体取决于Redis的版本历史,但广泛支持)
- 时间复杂度:O(n),n 是索引的偏移量,但实际上Redis的实现通常使得这个操作非常快。
- 返回值:如果操作成功,返回 “OK”。如果索引超出范围,返回错误。
- 示例:
注意:对于lset
的时间复杂度,虽然理论上它可能与索引的偏移量成线性关系,但实际上Redis的内部实现通常会使得这个操作非常高效,因为Redis的列表数据结构(通常基于双向链表或类似结构)允许在常数时间内进行索引访问(在知道索引位置的情况下)。然而,为了符合通常的复杂度描述习惯,这里仍然使用了O(n)来表示。在实际应用中,这个操作的性能通常不是问题。
三、 常用命令小结
操作类型 | 命令 | 时间复杂度 |
---|---|---|
添加 | rpush key value [value …] | O(k),k 是元素个数 |
添加 | lpush key value [value …] | O(k),k 是元素个数 |
添加 | linsert key before | after pivot value |
查找 | lrange key start end | O(s+n),s 是 start 偏移量,n 是 start 到 end 的范围 |
查找 | lindex key index | O(n),n 是索引的偏移量 |
查找 | llen key | O(1) |
删除 | lpop key | O(1) |
删除 | rpop key | O(1) |
删除 | lrem key count value | O(k),k 是元素个数 |
删除 | ltrim key start end | O(k),k 是元素个数 |
修改 | lset key index value | O(n),n 是索引的偏移量 |
阻塞操作 | blpop | O(1) |
阻塞操作 | brpop | O(1) |
四、 内部编码方式
列表类型的内部编码有两种:
- ziplist(压缩列表):当列表的元素个数小于 list-max-ziplist-entries 配置(默认 512 个),同时列表中每个元素的长度都小于 list-max-ziplist-value 配置(默认 64 字节)时,Redis 会选用ziplist 来作为列表的内部编码实现来减少内存消耗。
- linkedlist(链表):当列表类型无法满足 ziplist 的条件时,Redis 会使用linkedlist 作为列表的内部实现。
- 当元素个数较少且没有大元素时,内部编码为 ziplist:
2. 当元素个数超过 512 时,内部编码为 linkedlist:
127.0.0.1:6379> rpush listkey e1 e2 e3 ... 省略 e512 e513
OK
127.0.0.1:6379> object encoding listkey
"linkedlist"
由于插入元素太多,这里就不演示了。
- 当某个元素的长度超过 64 字节时,内部编码为 linkedlist:
经过实际操作我们会发现实际上的类型都是quicklist,以下是有关quicklist的介绍。
quicklist的引入时间:
- Redis从3.2版本开始,对列表数据结构进行了改造,引入了quicklist这一新的数据结构来替换之前的ziplist和linkedlist。这一改动旨在解决链表(linkedlist)内存占用高、碎片化严重以及压缩列表(ziplist)在元素较多时查询效率低的问题。
quicklist的特点:
- quicklist是ziplist和linkedlist的混合体,它将linkedlist按段切分,每一段使用ziplist来紧凑存储,多个ziplist之间使用双向指针串接起来。
- quicklist通过合理配置ziplist的大小和压缩深度,可以在保证操作效率的同时,最大限度地减少内存占用和碎片化。
- quicklist的设计使得Redis在处理列表数据时,既能够高效地添加、删除元素(特别是在列表两端),又能够在元素数量较多时保持较好的查询性能。
五、 典型使用场景
消息队列
如图 2-22 所示,Redis 可以使用 lpush + brpop 命令组合实现经典的阻塞式生产者-消费者模型队列,生产者客户端使用 lpush 从列表左侧插入元素,多个消费者客户端使用 brpop 命令阻塞式地从队列中"争抢" 队首元素。通过多个客户端来保证消费的负载均衡和高可用性。
分频道的消息队列
如图 2-23 所示,Redis 同样使用 lpush + brpop 命令,但通过不同的键模拟频道的概念,不同的消费者可以通过 brpop 不同的键值,实现订阅不同频道的理念。
微博 Timeline
每个用户都有属于自己的 Timeline(微博列表),现需要分页展示文章列表。此时可以考虑使用列表,因为列表不但是有序的,同时支持按照索引范围获取元素。
- 每篇微博使用哈希结构存储,例如微博中 3 个属性:title、timestamp、content:
hmset mblog:1 title xx timestamp 1476536196 content xxxxx
...
hmset mblog:n title xx timestamp 1476536196 content xxxxx
- 向用户 Timeline 添加微博,user::mblogs 作为微博的键:
lpush user:1:mblogs mblog:1 mblog:3
...
lpush user:k:mblogs mblog:9
- 分页获取用户的 Timeline,例如获取用户 1 的前 10 篇微博:
keylist = lrange user:1:mblogs 0 9
for key in keylist {
hgetall key
}
此方案在实际中可能存在两个问题:
- 1 + n 问题。即如果每次分页获取的微博个数较多,需要执行多次 hgetall 操作,此时可以考虑使用pipeline(流水线)模式批量提交命令,或者微博不采用哈希类型,而是使用序列化的字符串类型,使用 mget 获取。
- 分裂获取文章时,lrange 在列表两端表现较好,获取列表中间的元素表现较差,此时可以考虑将列表做拆分。