【Redis】列表类型详解及实际应用场景分析:从命令操作到内部编码


List 列表

列表类型是⽤来存储多个有序的字符串,如图 2-19 所⽰,a、b、c、d、e 五个元素从左到右组成了⼀个有序的列表,列表中的每个字符串称为元素(element),⼀个列表最多可以存储 2^32 - 1 个元素。在 Redis 中,可以对列表两端插⼊(push)和弹出(pop),还可以获取指定范围的元素列表获取指定索引下标的元素等(如图 2-19 和图 2-20 所⽰)。列表是⼀种⽐较灵活的数据结构,它可以充当栈和队列的⻆⾊,在实际开发上有很多应⽤场景。

Redis 的下标支持负数下标,负数下标相当于从后往前数

list 内部的结果(编码方式)并非是一个简单的数组,而更像是一个双端队列

图 2-19 列表两端插⼊和弹出操作

图 2-20 列表的获取、删除等操作

列表类型的特点:

  1. 列表中的元素是有序的,这意味着可以通过索引下标获取某个元素或者某个范围的元素列表,例如要获取图 2-20 的第 5 个元素,可以执⾏ lindex user:1:messages 4 或者倒数第 1 个元素,lindexuser:1:messages -1 就可以得到元素 e。

这里的有序不是指升序降序,这里的有序是指顺序很关键

比如如果把当前元素位置颠倒,顺序调换,得到的新的 list 和旧的 list 是不等价的

  1. 区分获取和删除的区别,例如图 2-20 中的 lrem 1 b 是从列表中把从左数遇到的前 1 个 b 元素删除,这个操作会导致列表的⻓度从 5 变成 4;但是执⾏ lindex 4 只会获取元素,但列表⻓度是不会变化的。

  2. 列表中的元素是允许重复的,例如图 2-21 中的列表中是包含了两个 a 元素的。

像 hash 的 field 是不能重复的

图 2-21 列表中允许有重复元素

命令

LPUSH

lpush

将⼀个或者多个元素从左侧放⼊(头插)到 list 中。这个头插是按顺序的,如果是多个,比如 1,2,3,4 那么最后 4 是在头部的。

如果 key 已经存在,并且 key 对应的 value 类型不是 list,这个命令就会报错

语法:

LPUSH key element [element ...]

命令有效版本:1.0.0 之后

时间复杂度:只插⼊⼀个元素为 O(1), 插⼊多个元素为 O(N), N 为插⼊元素个数.

返回值:插⼊后 list 的⻓度。

⽰例:

redis> LPUSH mylist "world"
(integer) 1
redis> LPUSH mylist "hello"
(integer) 2
redis> LRANGE mylist 0 -1
1) "hello"
2) "world"

LPUSHX

lpushx

x 是指 exists

在 key 存在时,将⼀个或者多个元素从左侧放⼊(头插)到 list 中。不存在,直接返回

语法:

LPUSHX key element [element ...]

命令有效版本:2.0.0 之后

时间复杂度:只插⼊⼀个元素为 O(1), 插⼊多个元素为 O(N), N 为插⼊元素个数.

返回值:插⼊后 list 的⻓度。

⽰例:

redis> LPUSH mylist "World"
(integer) 1
redis> LPUSHX mylist "Hello"
(integer) 2
redis> LPUSHX myotherlist "Hello"
(integer) 0
redis> LRANGE mylist 0 -1
1) "Hello"
2) "World"
redis> LRANGE myotherlist 0 -1
(empty array)

RPUSH

rpush

将⼀个或者多个元素从右侧放⼊(尾插)到 list 中。这个尾插是按顺序的,如果是多个,比如 1,2,3,4 那么最后 4 是在尾部的。

语法:

RPUSH key element [element ...]

命令有效版本:1.0.0 之后

时间复杂度:只插⼊⼀个元素为 O(1), 插⼊多个元素为 O(N), N 为插⼊元素个数.

返回值:插⼊后 list 的⻓度。

⽰例:

redis> RPUSH mylist "world"
(integer) 1
redis> RPUSH mylist "hello"
(integer) 2
redis> LRANGE mylist 0 -1
1) "world"
2) "hello"

RPUSHX

rpushx

x 是指 exists

在 key 存在时,将⼀个或者多个元素从右侧放⼊(尾插)到 list 中。

语法:

RPUSHX key element [element ...]

命令有效版本:2.0.0 之后

时间复杂度:只插⼊⼀个元素为 O(1), 插⼊多个元素为 O(N), N 为插⼊元素个数.

返回值:插⼊后 list 的⻓度。

⽰例:

redis> RPUSH mylist "World"
(integer) 1
redis> RPUSHX mylist "Hello"
(integer) 2
redis> RPUSHX myotherlist "Hello"
(integer) 0
redis> LRANGE mylist 0 -1
1) "World"
2) "Hello"
redis> LRANGE myotherlist 0 -1
(empty array)

LRANGE

lrange

list range,这个 l 不是指 left,因此没有 rrange

获取从 start 到 stop 区间的所有元素,左闭右闭。

一般用 lrange key 0 -1 查询整个 list

语法:

LRANGE key start stop

命令有效版本:1.0.0 之后

时间复杂度:O(N)

返回值:指定区间的元素。

⽰例:

redis> RPUSH mylist "one"
(integer) 1
redis> RPUSH mylist "two"
(integer) 2
redis> RPUSH mylist "three"
(integer) 3
redis> LRANGE mylist 0 0
1) "one"
redis> LRANGE mylist -3 2
1) "one"
2) "two"
3) "three"
redis> LRANGE mylist -100 100
1) "one"
2) "two"
3) "three"
redis> LRANGE mylist 5 10
(empty array)

如果下标超出范围了,在 C++ 中,会认为这是一个“未定义行为”,在 Java 中,一般会抛出异常。

在 Redis 中并没有这两种设定,Redis 的做法是尽可能获取到给定区间的元素。如果区间非法,就会尽可能的获取到对应的元素

对于 C++ 的处理方式:

  • 缺点:不一定能第一时间发现问题
  • 优点:机器执行效率最高

对于 Java 的处理方式:

  • 缺点:多出一步下标合法性的验证,机器执行效率低了
  • 优点:能第一时间发现问题

对于 Redis 的处理方式:

  • 优点:程序的容错能力更强

LPOP

lpop

从 list 左侧取出元素(即头删)。

语法:

LPOP key

命令有效版本:1.0.0 之后

时间复杂度:O(1)

返回值:取出的元素或者 nil。

⽰例:

redis> RPUSH mylist "one" "two" "three" "four" "five"
(integer) 5
redis> LPOP mylist
"one"
redis> LPOP mylist
"two"
redis> LPOP mylist
"three"
redis> LRANGE mylist 0 -1
1) "four"
2) "five"

RPOP

rpop

从 list 右侧取出元素(即尾删)。

语法:

RPOP key

注意:在 Redis 6.2 版本以后,这个命令多了个参数 count,这个参数是描述这次要删除多少个元素,语法格式是这样的,rpop key [count],同理有 lpop key [count]

命令有效版本:1.0.0 之后

时间复杂度:O(1)

返回值:取出的元素或者 nil。

⽰例:

redis> RPUSH mylist "one" "two" "three" "four" "five"
(integer) 5
redis> RPOP mylist
"five"
redis> LRANGE mylist 0 -1
1) "one"
2) "two"
3) "three"
4) "four"

命令用到这里可以感觉到 Redis 的 list 是一个双端队列,从两头插入/删除都是 O(1)

比如搭配使用 rpush 和 lpop 就相当于队列了,搭配使用 rpush 和 rpop 就相当于栈了,反过来同理

LINDEX

lindex

获取从左数第 index 位置的元素。

语法:

LINDEX key index

命令有效版本:1.0.0 之后

时间复杂度:O(N)

返回值:取出的元素或者 nil(下标非法的情况)。

⽰例:

redis> LPUSH mylist "World"
(integer) 1
redis> LPUSH mylist "Hello"
(integer) 2
redis> LINDEX mylist 0
"Hello"
redis> LINDEX mylist -1
"World"
redis> LINDEX mylist 3
(nil)

LINSERT

linsert

在特定位置插⼊元素。

语法:

LINSERT key <BEFORE | AFTER> pivot element

before/after 表示要插入指定元素之前还是之后,pivot 表示基准元素(不是下标),element 是指要插入的元素

如果基准值存在多个,则取从左往右的第一个基准值。这是因为 linsert 进行插入时,要根据基准值,从左往右找到对应的位置,找到第一个即可

命令有效版本:2.2.0 之后

时间复杂度:O(N),N 表示列表的长度

返回值:插⼊后的 list ⻓度。

⽰例:

redis> RPUSH mylist "Hello"
(integer) 1
redis> RPUSH mylist "World"
(integer) 2
redis> LINSERT mylist BEFORE "World" "There"
(integer) 3
redis> LRANGE mylist 0 -1
1) "Hello"
2) "There"
3) "World"

LLEN

llen

获取 list ⻓度。

语法:

LLEN key

命令有效版本:1.0.0 之后

时间复杂度:O(1)

返回值:list 的⻓度。

⽰例:

redis> LPUSH mylist "World"
(integer) 1
redis> LPUSH mylist "Hello"
(integer) 2
redis> LLEN mylist
(integer) 2

LREM

lrem

lrem,list remove

删除指定个数的元素

语法:

LREM key count element

count 表示要删除的个数,element 表示要删除的值。

  1. 如果 count > 0,表示从左往右删除指定元素 count 次
  2. 如果 count = 0,表示删除所有指定元素
  3. 如果 count < 0,表示从右往左删除指定元素 count 次

命令有效版本:1.0.0 之后

时间复杂度:O(N+M),N 表示 list 的长度,M 表示 count

返回值:成功删除的元素个数

示例:

redis> RPUSH mylist "hello"
(integer) 1
redis> RPUSH mylist "hello"
(integer) 2
redis> RPUSH mylist "foo"
(integer) 3
redis> RPUSH mylist "hello"
(integer) 4
redis> LREM mylist -2 "hello"
(integer) 2
redis> LRANGE mylist 0 -1
1) "hello"
2) "foo"

LTRIM

ltrim

对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。左闭右闭

语法:

LTRIM key start stop

命令有效版本:1.0.0 之后

时间复杂度:O(N),N 为被移除的元素的数量。

返回值:简单字符串回复,命令执行成功时返回 OK。

示例:

redis> RPUSH mylist "one"
(integer) 1
redis> RPUSH mylist "two"
(integer) 2
redis> RPUSH mylist "three"
(integer) 3
redis> LTRIM mylist 1 2
"OK"
redis> LRANGE mylist 0 -1
1) "two"
2) "three"

ACL categories:access control list,访问控制列表,即权限。这是 Redis 6 开始支持的,其实 5 也有,不过 6 才开始很详细、ACL 这块会给每个命令打上一些标签,管理员就可以给每个 Redis 用户配置不同的权限(允许该用户可以执行哪些标签对应的命令)

LSET

lset

将列表中指定位置的元素设置为另一个值。

语法:

LSET key index value

命令有效版本:1.0.0 之后

时间复杂度:O(N),N 为列表的长度。

返回值:简单字符串回复,命令执行成功时返回 OK。如果下标非法会报错

示例:

redis> RPUSH mylist "one"
(integer) 1
redis> RPUSH mylist "two"
(integer) 2
redis> RPUSH mylist "three"
(integer) 3
redis> LSET mylist 0 "four"
"OK"
redis> LSET mylist -2 "five"
"OK"
redis> LRANGE mylist 0 -1
1) "four"
2) "five"
3) "three"
阻塞版本命令

b,block,阻塞:当前的线程不走了,代码不继续执行了,会在满足一定的条件之后被唤醒。

blpop 和 brpop 是 lpop 和 rpop 的阻塞版本,和对应⾮阻塞版本的作⽤基本⼀致,除了:

  • 在列表中有元素的情况下,阻塞和⾮阻塞表现是⼀致的。但如果列表中没有元素非阻塞版本会理解返回 nil,但阻塞版本会根据 timeout,阻塞⼀段时间,期间 Redis 可以执⾏其他命令,但要求执⾏该命令的客⼾端会表现为阻塞状态(如图 2-22 所⽰)。

  • 命令中如果设置了多个键,那么会从左向右进⾏遍历键,⼀旦有⼀个键对应的列表中可以弹出元素命令立即返回

  • 如果多个客⼾端同时多一个键执行 pop,则最先执行命令的客户端会得到弹出的元素。

实现了一个“轮询”的效果

图 2-22 阻塞版本的 blpop 和 非阻塞版本 lpop 的区别

BLPOP

blpop

LPOP 的阻塞版本。

语法:

BLPOP key [key ...] timeout

单位是秒

Redis 6,超时时间允许设置成小数,单位依然是秒

Redis 5,超时时间只能设置成整数

命令有效版本:1.0.0 之后

时间复杂度:O(1)

返回值:取出的元素或者 nil。返回的结果相当于一个 pair,一个是告诉我们当前元素来自哪个 key,一个是告诉我们当前元素是哪个

⽰例:

redis> EXISTS list1 list2
(integer) 0
redis> RPUSH list1 a b c
(integer) 3
redis> BLPOP list1 list2 0
1) "list1"
2) "a"

如果这些 list 都为空,就需要阻塞等待,等待其他客户端往这些 list 中插入元素

BRPOP

brpop

RPOP 的阻塞版本。

语法:

BRPOP key [key ...] timeout

命令有效版本:1.0.0 之后

时间复杂度:O(1)

返回值:取出的元素或者 nil。

⽰例:

redis> DEL list1 list2
(integer) 0
redis> RPUSH list1 a b c
(integer) 3
redis> BRPOP list1 list2 0
1) "list1"
2) "c"

这两个阻塞命令主要作用在“消息队列”。不过其他场景功能还是比较有限的。而且 Redis 作为消息队列也不够其他消息队列好,因此这两个阻塞命令应用场景不多

命令小结

有关列表的命令已经介绍完毕,表 2-5 是这些命令的作⽤和时间复杂度,开发⼈员可以参考。

表 2-5 列表命令

内部编码

列表类型的内部编码有两种:

  • ziplist(压缩列表):当列表的元素个数⼩于 list-max-ziplist-entries 配置(默认 512 个),同时列表中每个元素的⻓度都⼩于 list-max-ziplist-value 配置(默认 64 字节)时,Redis 会选⽤ziplist 来作为列表的内部编码实现来减少内存消耗。
  • linkedlist(链表):当列表类型⽆法满⾜ ziplist 的条件时,Redis 会使⽤ linkedlist 作为列表的内部实现。
  1. 当元素个数较少且没有⼤元素时,内部编码为 ziplist:
127.0.0.1:6379> rpush listkey e1 e2 e3
ok
127.0.0.1:6379> object encoding listkey
"ziplist"
  1. 当元素个数超过 512 时,内部编码为 linkedlist:
127.0.0.1:6379> rpush listkey e1 e2 e3 ... 省略 e512 e513
OK
127.0.0.1:6379> object encoding listkey
"linkedlist"
  1. 当某个元素的⻓度超过 64 字节时,内部编码为 linkedlist:
127.0.0.1:6379> rpush listkey "one string is bigger than 64 bytes ... 省略 ..."
OK
127.0.0.1:6379> object encoding listkey
"linkedlist"

这两种是比较旧版的,Redis 5 以后就不是这样的了,仅供参考

当前的内部编码是 quicklist,相当于 ziplist 和 linkedlist 的结合,整体是一个链表,但链表的每个节点是一个压缩列表

从 Redis 3.2 版本开始,引入了 quicklist 作为列表的默认内部编码,结合了 ziplistlinkedlist 的优点,以优化内存使用和操作效率。quicklist 通过将多个 ziplist 链接成一个双向链表来实现,每个 ziplist 节点存储列表中的一部分元素。

  1. 在列表元素较少且元素大小较小的情况下,quicklist 内部使用 ziplist 来存储这些元素,以减少内存消耗:
127.0.0.1:6379> rpush listkey e1 e2 e3
OK
127.0.0.1:6379> object encoding listkey
"quicklist"

在这种情况下,尽管内部编码显示为 quicklist,实际上列表中的每个 quicklist 节点都是以 ziplist 的形式存储元素,以保持高效的内存使用。

  1. 随着列表元素数量的增加,quicklist 会根据配置 list-max-ziplist-size 和其他相关参数,自动调整内部 ziplist 节点的数量和大小,保持操作效率:
127.0.0.1:6379> rpush listkey e1 e2 e3 ... 省略 ... e512 e513
OK
127.0.0.1:6379> object encoding listkey
"quicklist"

此时,quicklist 会根据元素的数量和大小,适当增加新的 ziplist 节点,或者在现有的 ziplist 节点之间分配元素,以优化操作性能。

  1. 当列表中包含大尺寸元素时,quicklist 依旧能够有效地管理这些元素,通过动态调整 ziplist 节点的大小和数量来适应:
127.0.0.1:6379> rpush listkey "one string is bigger than list-max-ziplist-value ... 省略 ..."
OK
127.0.0.1:6379> object encoding listkey
"quicklist"

在这种情况下,即便列表中包含大于 list-max-ziplist-value 配置的元素,quicklist 通过调整其内部的 ziplist 节点,依然能够高效地存储和管理列表元素,同时保持较低的内存占用和高性能的操作特性。

使用场景

消息队列

如图 2-22 所⽰,Redis 可以使⽤ lpush + brpop 命令组合实现经典的阻塞式⽣产者-消费者模型队列,⽣产者客⼾端使⽤ lpush 从列表左侧插⼊元素,多个消费者客⼾端使⽤ brpop 命令阻塞式地从队列中"争抢" 队⾸元素。通过多个客⼾端来保证消费的负载均衡和⾼可⽤性

图 2-22 Redis 阻塞消息队列模型

brpop 这样的命令设定就能实现一个“轮询”的效果,先执行 brpop 的消费者先拿到 lpush 进入列表的元素,后执行的后拿到下一个

分频道的消息队列

如图 2-23 所⽰,Redis 同样使⽤ lpush + brpop 命令,但通过不同的键模拟频道的概念,不同的消费者可以通过 brpop 不同的键值,实现订阅不同频道的理念。这样就可以在某种数据发生的问题的时候,不会对其他数据造成影响(解耦合)

类似抖音有这样的实现,比如一个频道传输短视频数据,一个频道传输弹幕,一个频道传输点赞、收藏、转发,一个频道传输评论数据等等。

图 2-23 Redis 分频道阻塞消息队列模型

微博 Timeline

每个⽤⼾都有属于⾃⼰的 Timeline(微博列表),现需要分⻚展⽰⽂章列表。此时可以考虑使⽤列表,因为列表不但是有序的,同时⽀持按照索引范围获取元素。

  1. 每篇微博使⽤哈希结构存储,例如微博中 3 个属性:title、timestamp、content:
hmset mblog:1 title xx timestamp 1476536196 content xxxxx
...
hmset mblog:n title xx timestamp 1476536196 content xxxxx
  1. 向⽤⼾ Timeline 添加微博,user:<uid>:mblogs 作为微博的键:
lpush user:1:mblogs mblog:1 mblog:3
...
lpush user:k:mblogs mblog:9
  1. 分⻚获取⽤⼾的 Timeline,例如获取⽤⼾ 1 的前 10 篇微博:
keylist = lrange user:1:mblogs 0 9
for key in keylist {
 hgetall key
}

此⽅案在实际中可能存在两个问题:

  1. 1 + n 问题。即如果每次分⻚获取的微博个数较多,需要执⾏多次 hgetall 操作,此时可以考虑使⽤ pipeline流水线)模式批量提交命令,或者微博不采⽤哈希类型,⽽是使⽤序列化的字符串类型,使⽤ mget 获取。

相当于把多个命令合并成一个网络请求进行通信,大大降低客户端和服务器的交互次数了

  1. 分裂获取⽂章时,lrange 在列表两端表现较好,获取列表中间的元素表现较差,此时可以考虑将列表做拆分

选择列表类型时,请参考:

同侧存取(lpush + lpop 或者 rpush + rpop)为

异侧存取(lpush + rpop 或者 rpush + lpop)为队列

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值