Skip to content

列表

列表(list)类型是用来存储多个有序的字符串,列表中的每个字符串称为元素,一个列表最多可以存储 2^{32} - 1 个元素。
在 Redis 中,可以对列表两端插入(push)和弹出(pop),还可以获取指定范围的元素列表、获取指定索引下标的元素等。
列表是一种比较灵活的数据结构,它可以充当栈和队列的角色,在实际开发上有很多应用场景。

列表类型有两个特点:
第一、列表中的元素是有序的,这就意味着可以通过索引下标获取某个元素后者某个范围内的元素列表,例如要获取第 5 个元素,可以执行 lindex user:1:message 4(索引从 0 算起)

第二、列表中的元素可以是重复的

命令

操作类型 操作
添加 rpush、lpush、linsert
lrange lindex llen
删除 lpop rpop lrem ltrim
修改 lset
阻塞操作 blpop brpop

添加操作

从右边插入元素

rpush key value [value...]

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

1
2
127.0.0.1:6379> rpush listkey c b a
(integer) 3

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

1
2
3
4
127.0.0.1:6379> lrange listkey 0 -1
1) "c"
2) "b"
3) "a"

从左边插入元素

lpush key value [value...]

向某个元素前或者后插入元素

linsert key before | after pivot value

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

1
2
127.0.0.1:6379> linsert listkey before b java
(integer) 4

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

1
2
3
4
5
127.0.0.1:6379> lrange listkey 0 -1
1) "c"
2) "java"
3) "b"
4) "a"

查找

获取指定范围内的元素列表

lrange key start end

lrange 操作会获取列表指定索引范围所有的元素。
索引下标有两个特点: 第一,索引下标从左到右分别是 0 到 N-1,但是从右到左分别是 -1 到 -N。
第二,lrange 中的 end 选项包括了自身,这个和很多编程语言不包括 end 不太相同,例如想获取列表的第 2 到第 4 个元素,可以执行如下操作:

1
2
3
4
127.0.0.1:6379> lrange listkey 1 3
1) "java"
2) "b"
3) "a"

获取列表指定索引下标的元素

lindex key index

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

1
2
127.0.0.1:6379> lindex listkey -1
"a"

获取列表长度

llen key

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

1
2
127.0.0.1:6379> llen listkey
(integer) 4

删除

从列表左侧弹出元素

lpop key

如下操作将列表最左侧的元素 c 会被弹出,弹出后列表变为 java、b、a:

1
2
3
4
5
6
127.0.0.1:6379> lpop listkey
"c"
127.0.0.1:6379> lrange listkey 0 -1
1) "java"
2) "b"
3) "a"

从列表右侧弹出

rpop key

删除指定元素

lrem key count value

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

  • count>0,从左到右,删除最多 count 个元素
  • count<0, 从右到左,删除最多 count 绝对值个元素
  • count=0, 删除所有

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
127.0.0.1:6379> lpush listkey a a a a a
(integer) 8
127.0.0.1:6379> lrange listkey 0 -1
1) "a"
2) "a"
3) "a"
4) "a"
5) "a"
6) "java"
7) "b"
8) "a"
127.0.0.1:6379> lrem listkey 4 a
(integer) 4
127.0.0.1:6379> lrange listkey 0 -1
1) "a"
2) "java"
3) "b"
4) "a"

按照索引范围修剪列表

ltrim key start end

例如,下面操作会只保留列表 listkey 第二个到第四个元素:

1
2
3
4
5
6
127.0.0.1:6379> ltrim listkey 1 3
OK
127.0.0.1:6379> lrange listkey 0 -1
1) "java"
2) "b"
3) "a"

修改

修改指定索引下标的元素:

lset key index newValue

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

1
2
3
4
5
6
127.0.0.1:6379> lset listkey 2 python
OK
127.0.0.1:6379> lrange listkey 0 -1
1) "java"
2) "b"
3) "python"

阻塞操作

阻塞式弹出如下:

1
2
blpop key [key...] timeout
brpop key [key...] timeout

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

  • key[key...]: 多个列表的键
  • timeout: 阻塞时间(单位: 秒)

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

  • key[key...]: 多个列表的键
  • timeout: 阻塞时间(单位: 秒)

(1) 列表为空: 如果 timeout=3,那么客户端要等到 3 秒后返回,如果 timeout=0,那么客户端一直阻塞等下去:

1
2
3
127.0.0.1:6379> brpop list:test 3
(nil)
(3.07s)
1
2
127.0.0.1:6379> brpop list:test 0
...阻塞...

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

1
2
3
4
5
127.0.0.1:6379> rpush list:test element1
(integer) 1
127.0.0.1:6379> brpop list:test 3
1) "list:test"
2) "element1"

(2) 列表不为空: 客户端会立即返回

1
2
3
4
5
127.0.0.1:6379> rpush list:test element1
(integer) 1
127.0.0.1:6379> brpop list:test 0
1) "list:test"
2) "element1"

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

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

1
2
127.0.0.1:6379> brpop list:1 list:2 list:3 0
...阻塞...

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

1
2
127.0.0.1:6379> lpush list:2 element2
(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" (51.93s) ···

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

内部编码

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

ziplist(压缩列表)

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

linkedlist(链表)

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

python使用

方法 作用 示例 示例结果
lpush(name, *values) 在键名为name的列表头添加值为value的元素,可以传多个 redis.lpush('list', 0) redis.lpush('list', 1, 2) 1(列表大小) 3
rpush(name, *values) 在键名为name的列表末尾添加值为value的元素,可以传多个 redis.rpush('list', 4, 5, 6) 6
llen(name) 返回键名为name的列表的长度 redis.llen('list') 6
lrange(name, start, end) 返回键名为name的列表中start至end之间的元素 redis.lrange('list', 0, 3) redis.lrange('list', 0, -1) [b'2', b'1', b'0', b'4'] [b'2', b'1', b'0', b'1', b'0', b'4', b'5', b'6']
ltrim(name, start, end) 截取键名为name的列表,保留索引为start到end的内容 redis.ltrim('list', 1, 3) redis.llen('list') True 3
lpop(name) 返回并删除键名为name的列表中的首元素 redis.lpop('list') b'1'
rpop(name) 返回并删除键名为name的列表中的尾元素 redis.rpop('list') b'4'
lpushx/rpushx(name, value) key存在时才进行处理,key不存在时不做任何处理 redis.lpushx('hhh', 1) 0

技巧:

  • 使用ltrim(name, 1, 0)可以清空 key 为 name 的 列表
1
2
3
4
5
6
7
8
from redis import StrictRedis
REDIS_CLI = StrictRedis(
    host="127.0.0.1",
    port=6379,
    db=0,
)
l = [1, 2, 3]
REDIS_CLI.lpush("fund_nums", *l)

使用场景

消息队列

Redis 的 lpush + brpop 命令组合即可实现阻塞队列,生产者客户端使用 lpush 从列表左侧插入元素,多个消费者客户端使用 brpop 命令阻塞式的 "抢" 列表尾部的元素,多个客户端保证了消费的负载均衡和高可用

文章列表

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

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

1
2
3
4
hmset article:1 title xx timestamp 1476536196 content xxx
...
hmset article:k title yy timestamp 1476512536 content yyyy
...

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

1
2
3
4
lpush user:1:articles article:1 article3
...
lpush user:k:articles article:5
...

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

1
2
3
articles = lrange user:1:articles 0 9
for article in {articles}
    hgetall {article}

使用列表类型保存和获取文章列表会存在两个问题。
第一,如果每次分页获取的文章个数较多,需要执行多次 hgetall 操作,此时可以考虑使用 Pipeline 批量获取,或者考虑将文章数据序列化为字符串类型,使用 mget 批量获取。
第二,分页获取文章列表时,lrange 命令在列表两端性能较好,但是如果列表较大,获取列表中间范围的元素性能会变差,此时可以考虑将列表做二级拆分

开发提示

实际上列表的使用场景很多,在选择时可以参考以下:

  • lpush + lpop = Stack(栈)
  • lpush + rpop=Queue(队列)
  • lpush + ltrim=Capped Collection (有限集合)
  • lpush+brpop=Message Queue(消息队列)