深入剖析Redis系列之一 - Redis数据结构之列表

20 篇文章 0 订阅
20 篇文章 0 订阅

前言

列表( list)类型是用来存储多个 有序 的 字符串。在 Redis 中,可以对列表的 两端 进行 插入( push)和 弹出( pop)操作,还可以获取 指定范围 的 元素列表、获取 指定索引下标 的 元素 等。
Redis

列表 是一种比较 灵活 的 数据结构,它可以充当 栈 和 队列 的角色,在实际开发上有很多应用场景。

如图所示, a、 b、 c、 d、 e 五个元素 从左到右 组成了一个 有序的列表,列表中的每个字符串称为 元素( element),一个列表最多可以存储 2^32-1 个元素。

列表的 插入 和 弹出 操作
Redis

列表的 获取、截取 和 删除 操作
Redis

正文

  1. 相关命令
    下面将按照对 列表 的 5 种 操作类型 对命令进行介绍:
    在这里插入图片描述

1.1. 添加命令
1.1.1. 从右边插入元素
rpush key value [value …]

下面代码 从右向左 插入元素 c、 b、 a:

127.0
.
0.1
:
6379

rpush listkey c b a

(integer)
3

lrange0-1 命令可以 从左到右 获取列表的 所有元素:

127.0
.
0.1
:
6379

lrange listkey
0

1

1
)
“c”

2
)
“b”

3
)
“a”

1.1.2. 从左边插入元素
lpush key value [value …]

使用方法和 rpush 相同,只不过从 左侧插入,这里不再赘述。

1.1.3. 向某个元素前或者后插入元素
linsert key before|after pivot value

linsert 命令会从 列表 中找到 第一个 等于 pivot 的元素,在其 前( before)或者 后( after)插入一个新的元素 value,例如下面操作会在列表的 元素 b 前插入 redis:

127.0
.
0.1
:
6379

linsert listkey before b redis

(integer)
4

返回结果为 4,代表当前 列表 的 长度,当前列表变为:

127.0
.
0.1
:
6379

lrange listkey
0

1

1
)
“c”

2
)
“redis”

3
)
“b”

4
)
“a”

1.2. 查询命令
1.2.1. 获取指定范围内的元素列表
lrange key start stop

lrange 操作会获取列表 指定索引 范围所有的元素。

索引下标 有两个特点:

其一,索引下标 从左到右 分别是 0 到 N-1,但是 从右到左 分别是 -1 到 -N。

其二, lrange 中的 end 选项包含了 自身,这个和很多编程语言不包含 end 不太相同。

从左到右 获取列表的第 2 到第 4 个元素,可以执行如下操作:

127.0
.
0.1
:
6379

lrange listkey
1

3

1
)
“redis”

2
)
“b”

3
)
“a”

从右到左 获取列表的第 1 到第 3 个元素,可以执行如下操作:

127.0
.
0.1
:
6379

lrange listkey -
3

1

1
)
“redis”

2
)
“b”

3
)
“a”

1.2.2. 获取列表指定索引下标的元素
lindex key index

例如当前列表 最后一个 元素为 a:

127.0
.
0.1
:
6379

lindex listkey -
1

“a”

1.2.3. 获取列表长度
llen key

例如,下面示例 当前列表长度 为 4:

127.0
.
0.1
:
6379

llen listkey

(integer)
4

1.3. 删除命令
1.3.1. 从列表左侧弹出元素
lpop key

如下操作将 列表 最左侧的元素 c 弹出,弹出后 列表 变为 redis、 b、 a。

127.0
.
0.1
:
6379

lpop listkey

“c”

127.0
.
0.1
:
6379

lrange listkey
0

1

1
)
“redis”

2
)
“b”

3
)
“a”

1.3.2. 从列表右侧弹出元素
rpop key

它的使用方法和 lpop 是一样的,只不过从列表 右侧 弹出元元素。

127.0
.
0.1
:
6379

lpop listkey

“a”

127.0
.
0.1
:
6379

lrange listkey
0

1

1
)
“c”

2
)
“redis”

3
)
“b”

1.3.3. 删除指定元素
lrem key count value

lrem 命令会从 列表 中找到 等于 value 的元素进行 删除,根据 count 的不同分为三种情况:

count > 0:从左到右,删除最多 count 个元素。

count < 0:从右到左,删除最多 count 绝对值 个元素。

count = 0,删除所有。

例如向列表 从左向右 插入 5 个 a,那么当前 列表 变为 “a a a a a redis b a”,下面操作将从列表 左边 开始删除 4 个为 a 的元素:

127.0
.
0.1
:
6379

lrem listkey
4
a

(integer)
4

127.0
.
0.1
:
6379

lrange listkey
0

1

1
)
“a”

2
)
“redis”

3
)
“b”

4
)
“a”

1.3.4. 按照索引范围修剪列表
127.0
.
0.1
:
6379

ltrim listkey
1

3

OK

127.0
.
0.1
:
6379

lrange listkey
0

1

1
)
“redis”

2
)
“b”

3
)
“a”

1.4. 修改命令
1.4.1. 修改指定索引下标的元素
修改 指定索引下标 的元素:

lset key index newValue

下面操作会将列表 listkey 中的第 3 个元素设置为 mysql:

127.0
.
0.1
:
6379

lset listkey
2
mysql

OK

127.0
.
0.1
:
6379

lrange listkey
0

1

1
)
“redis”

2
)
“b”

3
)
“mysql”

1.5. 阻塞操作命令
阻塞式弹出 操作的命令如下:

blpop key [key …] timeout
brpop key [key …] timeout

blpop 和 brpop 是 lpop 和 rpop 的 阻塞版本,它们除了 弹出方向 不同,使用方法 基本相同,所以下面以 brpop 命令进行说明, brpop 命令包含两个参数:

key[key…]:一个列表的 多个键。

timeout:阻塞 时间(单位:秒)。

对于 timeout 参数,要氛围 列表为空 和 不为空 两种情况:

列表为空

如果 timeout=3,那么 客户端 要等到 3 秒后返回,如果 timeout=0,那么 客户端 一直 阻塞 等下去:

127.0
.
0.1
:
6379

brpop list:test
3

(nil)

(
3.10s
)

127.0
.
0.1
:
6379

brpop list:test
0

…阻塞…

如果此期间添加了数据 element1,客户端 立即返回:

127.0
.
0.1
:
6379

brpop list:test
3

1
)
“list:test”

2
)
“element1”

(
2.06s
)

列表不为空:客户端会 立即返回。

127.0
.
0.1
:
6379

brpop list:test
0

1
)
“list:test”

2
)
“element1”

在使用 brpop 时,有以下两点需要注意:

其一,如果是 多个键,那么 brpop 会 从左至右 遍历键,一旦有 一个键 能 弹出元素,客户端 立即返回:

127.0
.
0.1
:
6379

brpop list:
1
list:
2
list:
3

0

…阻塞…

此时另一个 客户端 分别向 list:2 和 list:3 插入元素:

client-lpush> lpush list:
2
element2

(integer)
1

client-lpush> lpush list:
3
element3

(integer)
1

客户端 会立即返回 list:2 中的 element2,因为 list:2 最先有 可以弹出 的元素。

127.0
.
0.1
:
6379

brpop list:
1
list:
2
list:
3

0

1
)
“list:2”

2
)
“element2”

其二,如果 多个客户端 对 同一个键 执行 brpop,那么 最先执行 brpop 命令的 客户端 可以 获取到弹出的值。

按先后顺序在 3 个客户端执行 brpop 命令:

客户端1:

client-
1

brpop list:test
0

…阻塞…

客户端2:

client-
2

brpop list:test
0

…阻塞…

客户端3:

client-
3

brpop list:test
0

…阻塞…

此时另一个 客户端 lpush 一个元素到 list:test 列表中:

client-lpush> lpush list:test element

(integer)
1

那么 客户端 1 会获取到元素,因为 客户端 1 最先执行 brpop 命令,而 客户端 2 和 客户端 3 会继续 阻塞。

client> brpop list:test
0

1
)
“list:test”

2
)
“element”

有关 列表 的 基础命令 已经介绍完了,下表是相关命令的 时间复杂度:

Redis

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

2.1. ziplist(压缩列表)
当列表的元素个数 小于 list-max-ziplist-entries 配置(默认 512 个),同时列表中 每个元素 的值都 小于 list-max-ziplist-value 配置时(默认 64 字节), Redis会选用 ziplist 来作为 列表 的 内部实现 来减少内存的使用。

2.2. linkedlist(链表)
当 列表类型 无法满足 ziplist 的条件时, Redis 会使用 linkedlist 作为 列表 的 内部实现。

2.3. 编码转换
下面的示例演示了 列表类型 的 内部编码,以及相应的变化。

当元素 个数较少 且 没有大元素 时,内部编码 为 ziplist:

127.0
.
0.1
:
6379

rpush listkey e1 e2 e3

(integer)
3

127.0
.
0.1
:
6379

object encoding listkey

“ziplist”

当元素个数超过 512 个,内部编码 变为 linkedlist:

127.0
.
0.1
:
6379

rpush listkey e4 e5 … e512 e513

(integer)
513

127.0
.
0.1
:
6379

object encoding listkey

“linkedlist”

当某个元素超过 64 字节,内部编码 也会变为 linkedlist:

127.0
.
0.1
:
6379

rpush listkey
“one string is bigger than 64 byte…”

(integer)
4

127.0
.
0.1
:
6379

object encoding listkey

“linkedlist”

Redis3.2 版本提供了 quicklist 内部编码,简单地说它是以一个 ziplist 为 节点 的 linkedlist,它结合了 ziplist 和 linkedlist 两者的优势,为 列表类型 提供了一种更为优秀的 内部编码 实现,它的设计原理可以参考 Redis 的另一个作者 MattStancliff 的博客 redis-quicklist。

  1. 应用场景
    3.1. 消息队列
    通过 Redis 的 lpush+brpop 命令组合,即可实现 阻塞队列。如图所示:
    Redis

生产者客户端 使用 lrpush 从列表 左侧插入元素,多个消费者客户端 使用 brpop 命令 阻塞式 的 “抢” 列表 尾部 的元素,多个客户端 保证了消费的 负载均衡 和 高可用性。

3.2. 文章列表
每个 用户 有属于自己的 文章列表,现需要 分页 展示文章列表。此时可以考虑使用 列表,因为列表不但是 有序的,同时支持 按照索引范围 获取元素。

每篇文章使用 哈希结构 存储,例如每篇文章有 3 个属性 title、 timestamp、 content:

hmset acticle:
1
title xx timestamp
1476536196
content xxxx

hmset acticle:
2
title yy timestamp
1476536196
content yyyy

hmset acticle:k title kk timestamp
1476512536
content kkkk

向用户文章列表 添加文章, user:{id}:articles 作为用户文章列表的 键:

lpush user:
1
:acticles article:
1
article:
3
article:
5

lpush user:
2
:acticles article:
2
article:
4
article:
6

lpush user:k:acticles article:
7
article:
8

分页 获取 用户文章列表,例如下面 伪代码 获取用户 id=1 的前 10 篇文章:

articles = lrange user:
1
:articles
0

9

for
article
in
{articles}

hgetall {article}

使用 列表 类型 保存 和 获取 文章列表会存在两个问题:

第一:如果每次 分页 获取的 文章个数较多,需要执行多次 hgetall 操作,此时可以考虑使用 Pipeline 进行 批量获取,或者考虑将文章数据 序列化为字符串 类型,使用 mget 批量获取。

第二:分页 获取 文章列表 时, lrange 命令在列表 两端性能较好,但是如果 列表较大,获取列表 中间范围 的元素 性能会变差。此时可以考虑将列表做 二级拆分,或者使用 Redis3.2 的 quicklist 内部编码实现,它结合 ziplist 和 linkedlist 的特点,获取列表 中间范围 的元素时也可以 高效完成。

3.3. 其他场景
实际上列表的使用场景很多,具体可以参考如下:

命令组合 对应数据结构
lpush + lpop Stack(栈)
lpush + rpop Queue(队列)
lpush + ltrim Capped Collection(有限集合)
lpush + brpop Message Queue(消息队列)
小结
本文介绍了 Redis 中的 列表 的 一些 基本命令、内部编码 和 适用场景。通过组合不同 命令,可以把 列表 转换为不同的 数据结构 使用。

加Java架构师进阶交流群获取Java工程化、高性能及分布式、高性能、深入浅出。高架构。性能调优、Spring,MyBatis,Netty源码分析和大数据等多个知识点高级进阶干货的直播免费学习权限 都是大牛带飞 让你少走很多的弯路的 群号是:883922439 对了 小白勿进 最好是有开发经验

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值