
Redis 的 List 是通过 链表 实现的,因此:
列表类型用于 存储多个有序的字符串,如 a、b、c、d、e 五个元素从左到右组成一个有序列表,每个字符串称为 元素,最多可存储
个元素。
push)、弹出(pop)、获取指定范围或索引的元素等操作
特点
lindexuser:1:messsage 或者 lindexuser1:message-1lrem 1 b 删除列表中第一个 b 元素,而 lindex 4 仅获取元素,不影响列表长度。如图所示:

命令 | 描述 |
|---|---|
LPUSH/RPUSH key value [value ...] | 将一个或多个值插入到列表 头部/尾部 |
LPUSHX/RPUSHX key value [value ...] | 将一个或多个值插入到列表 头部/尾部,key 不存在不插入 |
LPOP key | 移除并返回列表头部元素 |
RPOP key | 移除并返回列表尾部元素 |
LRANGE key start stop | 获取列表中指定范围内的元素 |
LINDEX key index | 获取指定索引位置的元素 |
LLEN key | 返回列表长度 |
LREM key count value | 移除列表中与 value 相等的元素 |
LSET key index value | 设置指定索引位置的元素值 |
LTRIM key start stop | 对列表进行裁剪,只保留指定范围内的元素 |
BLPOP/BRPOP key [key ...] timeout | 阻塞 timeout 时间,若结束前无新元素到来则返回 nil,反之返回新元素 |
区别如下:
特性 | L/R PUSH key value [value ...] | L/R PUSHX key value [value ...] |
|---|---|---|
是否仅当键存在时才插入 | ❌ 否 | ✅ 是 |
如果 key 不存在会创建吗? | ✅ 会 | ❌ 不会 |
插入多个值 | ✅ 支持 | ✅ 支持 |
返回值含义 | 插入后列表的长度 | 插入后列表的长度(如果 key 不存在则返回 0) |
语法如下:
L/R PUSH/PUSHX key element [element ...]① LPUSH:从左侧插入元素,时间复杂度 O(N)(取决于插入元素个数)
list中的
LPUSH key 1 2 3 4,那么最后list呈现的结果为:4 3 2 1,采取的为头插
② LPUSHX:在key存在时,将⼀个或者多个元素从左侧放⼊(头插)到list中。不存在,直接返回
③ RPUSH:从右侧插入元素,时间复杂度 O(N)(取决于插入元素个数)
④ RPUSHX:若键存在,则从右侧插入元素,否则返回
案例如下:
127.0.0.1:6379> lpushx mylist "hello"
(integer) 0
127.0.0.1:6379> lpush mylist "hello"
(integer) 1
127.0.0.1:6379> rpush mylist "world"
(integer) 2
127.0.0.1:6379> LRANGE mylist 0 1
1) "hello"
2) "world"LPOP:从左侧弹出元素,时间复杂度 (O(1))
RPOP:从右侧弹出元素,时间复杂度 (O(1))
注意:可一次删除多个
案例如下:
127.0.0.1:6379> lpop mylist
"hello"
127.0.0.1:6379> LRANGE mylist 0 -1
1) "world"① LRANHGE:获取指定范围的元素,时间复杂度 (O(N))。
LRANGE key start stop注意:stop 为 -1时,相当于是 len - 1 查询从 start 开始的所有元素
127.0.0.1:6379> LRANGE mylist 0 1
1) "hello"
2) "world"
127.0.0.1:6379> LRANGE mylist 0 -1
1) "hello"
2) "world"② LINDEX:获取从左数第index位置的元素。时间复杂度:O(N),返回取出的元素或者nil
LINDEX key index③ LINSERT:从左侧开始的特定位置插入元素(如果有多个 pivot 元素,只在第一个 pivot 的位置插入)。时间复杂度:O(N),返回插入后的 List 长度
LINSERT key <BEFORE | AFTER> pivot element案例如下:
127.0.0.1:6379> linsert mylist AFTER "world" a
(integer) 2
127.0.0.1:6379> LINSERT mylist BEFORE "world" "hi"
(integer) 3
127.0.0.1:6379> LRANGE mylist 0 -1
1) "hi"
2) "world"
3) "a"
127.0.0.1:6379> lindex mylist 1
"world"① LLEN:获取 list 长度,时间复杂度:O(1),返回 list 长度。
LLEM key② LREM:rem 的意思是 remove,所以意思很明显,就是要移出某个元素
lrem key count element其中 count 表示的是要删除多少个元素,其中返回值表示的是删除成功的个数
count > 0:删除元素从头到尾count < 0:删除元素从尾到头count = 0:不删除案例如下:
127.0.0.1:6379> rpush list 1 2 3 hi 1 2 3 ho 1 2 3 hi
(integer) 12
127.0.0.1:6379> lrem list 2 hi
(integer) 2
127.0.0.1:6379> lrange list 0 -1
1) "1"
2) "2"
3) "3"
4) "1"
5) "2"
6) "3"
7) "ho"
8) "1"
9) "2"
10) "3"③ LSET:根据下标修改元素(支持负数下标,如果下标越界,会返回一个报错)
lset key index element④ LTRIM:保留 [start, stop] 闭区间的元素,其他元素全部删除
ltrim key start stop案例如下:
127.0.0.1:6379> rpush list 1 2 3 4 5
(integer) 5
127.0.0.1:6379> ltrim list 1 3
OK
127.0.0.1:6379> lrange list 0 -1
1) "2"
2) "3"
3) "4"
127.0.0.1:6379> lset list 1 666
OK
127.0.0.1:6379> lrange list 0 -1
1) "2"
2) "666"
3) "4"比如:在多线程中,有一个生产消费模型,其可以基于阻塞队列实现,主要满足以下两个性质:
在Redis中,list只考虑队列为空的情况,也就是消费者。用户读取数据时,队列为空,那么用户陷入阻塞,直到队列有数据。
阻塞 vs 非阻塞:
现有下面三种情况,图示如下:
情况一:列表不为空

情况二:列表为空,且 5s 内没有新元素加入

情况三:列表为空,但 5s 内有新元素加入
lpop user:1:messages 立即得到 nilblpop user:1:messages 执行命令,若 timeout 结束前,有新元素加入,则直接得到新元素命令使用,语法如下:
BLPOP/BRPOP key [key ...] timeout说明:
timeout,以秒为单位,超过时间则返回 nil。① BLPOP:读取并删除列表头部元素,如果列表为空则用户陷入阻塞,案例如下:
127.0.0.1:6379> lpush list1 1 2 3
(integer) 3
127.0.0.1:6379> blpop list1 list2 5
1) "list1"
2) "3"
127.0.0.1:6379> llen list2
(integer) 0
127.0.0.1:6379> blpop list2 10
(nil)
(10.02s)此处启用了两个客户端,左侧客户端blpop一个空列表,等待 10s,随后陷入阻塞。接着右侧客户端插入一个元素到list2,随后左侧客户端立刻拿到数据并进行头删
127.0.0.1:6379> lpush list2 1 # 启用第二个客户端
(integer) 1
127.0.0.1:6379> blpop list2 10
1) "list2"
2) "1"
(1.91s)② BRPOP:读取并删除列表尾部元素,如果列表为空则用户陷入阻塞(具体使用和上面类似,不过多讲解)
ziplist(压缩列表):一种内存紧凑的存储方式,适合存储数量较少且元素较小的列表。当列表的元素个数小于 list-max-ziplist-entries 配置(默认512个),同时列表中每个元素的长度都小于 list-max-ziplist-value 配置(默认64字节)时,Redis会选用 ziplist 来作为列表的内部编码实现来减少内存消耗。
linkedlist(链表):当列表类型无法满足 ziplist 条件时,使用 linkedlist 作为内部实现。

考虑到链表的附加空间相对太高,prev 和 next 指针就要占去 16 个字节 (64bit 系统的指针是 8 个字节),另外每个节点的内存都是单独分配,会加剧内存的碎片化,影响内存管理效率。因此Redis3.2版本开始对列表数据结构进行了改造,使用 quicklist 代替了 ziplist 和 linkedlist
结构:quicklist 实际上是 zipList 和 linkedList 的混合体,它将 linkedList 按段切分(外层列表仍然是 linkedlist 双链表结构),每个链表节点都是一个 ziplist,对中间部分的节点进行一定程度的压缩,提高效率,多个 zipList 之间使用双向指针串接起来

源码如下:
typedef struct quicklistNode {
struct quicklistNode *prev; //上一个node节点
struct quicklistNode *next; //下一个node
unsigned char *zl; //保存的数据 压缩前ziplist 压缩后压缩的数据
unsigned int sz; /* ziplist size in bytes */
unsigned int count : 16; /* count of items in ziplist */
unsigned int encoding : 2; /* RAW==1 or LZF==2 */
unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
unsigned int recompress : 1; /* was this node previous compressed? */
unsigned int attempted_compress : 1; /* node can't compress; too small */
unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;参数分析:
prev:指向链表前一个节点的指针。next:指向链表后一个节点的指针。zl:数据指针。如果当前节点的数据没有压缩,那么它指向一个ziplist结构;否则,它指向一个quicklistLZF结构。sz:表示zl指向的ziplist的总大小(包括zlbytes, zltail, zllen, zlend和各个数据项)。需要注意的是:如果ziplist被压缩了,那么这个sz的值仍然是压缩前的ziplist大小。count:表示ziplist里面包含的数据项个数。这个字段只有16bit。稍后我们会一起计算一下这16bit是否够用。encoding:表示ziplist是否压缩了(以及用了哪个压缩算法)。目前只有两种取值:2表示被压缩了(而且用的是LZF压缩算法),1表示没有压缩。container:是一个预留字段。本来设计是用来表明一个quicklist节点下面是直接存数据,还是使用ziplist存数据,或者用其它的结构来存数据(用作一个数据容器,所以叫container)。但是,在目前的实现中,这个值是一个固定的值2,表示使用ziplist作为数据容器。recompress:当我们使用类似lindex这样的命令查看了某一项本来压缩的数据时,需要把数据暂时解压,这时就设置recompress=1做一个标记,等有机会再把数据重新压缩。attempted_compress:这个值只对Redis的自动化测试程序有用。我们不用管它。extra:其它扩展字段。目前Redis的实现里也没用上。typedef struct quicklistLZF {
unsigned int sz; /* LZF size in bytes*/
char compressed[];
} quicklistLZF;quicklistLZF结构表示一个被压缩过的ziplist。其中:
typedef struct quicklist {
quicklistNode *head;
quicklistNode *tail;
unsigned long count; /* total count of all entries in all ziplists */
unsigned long len; /* number of quicklistNodes */
int fill : QL_FILL_BITS; /* fill factor for individual nodes */
unsigned int compress : QL_COMP_BITS; /* depth of end nodes not to compress;0=off */
unsigned int bookmark_count: QL_BM_BITS;
quicklistBookmark bookmarks[];
} quicklist;head:指向头节点(左侧第一个节点)的指针。tail:指向尾节点(右侧第一个节点)的指针。count:所有ziplist数据项的个数总和。len:quicklist节点的个数。fill:16bit,ziplist大小设置,存放list-max-ziplist-size参数的值。compress:16bit,节点压缩深度设置,存放list-compress-depth参数的值。实现:Redis 可以使用 **lpush ** + brpop 命令组合实现经典的 阻塞式生产者-消费者模型队列。
流程:
lpush 从列表左侧插入元素。brpop 命令阻塞式地从队列中“争抢”队首元素
特点:通过多个客户端来保证消费的负载均衡和高可用性,保证只有一个消费者能“抢到”元素
实现:Redis 同样使用 lpush + brpop 命令,但通过不同的键模拟频道的概念
流程:
brpop 不同的键值,实现订阅不同频道的理念
特点:每个频道只有一个消费者能“抢到”元素,不同的消费者可以订阅不同的频道,确保某个主题的数据出现问题时不会影响其他频道
思考:如何确定是哪个消费者“抢到”了元素?
示例:假设我们有两个消费者(Consumer A 和 Consumer B)订阅同一个频道 key-1,生产者将消息推送到 key-1。
生产者
lpush key-1 message1消费者 A/B
import redis
client = redis.StrictRedis()
def handle_message(message):
print(f"Consumer A got message: {message}")
# 记录日志
with open('consumer_a_log.txt', 'a') as log_file:
log_file.write(f"Consumer A got message: {message}\n")
while True:
message = client.brpop('key-1')
if message:
handle_message(message[1].decode('utf-8'))日志记录
Consumer A 的日志文件 consumer_a_log.txt:
Consumer A got message: message1Consumer B 的日志文件 consumer_b_log.txt,同上
总结如下,可以通过上述方法明确知道是哪个消费者 “抢到了" 元素
brpop 命令的结果。需求:每个用户都有属于自己的 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
此时博客目录 通过 list 将每篇博客数据(hash) 组织起来了
③ 分页获取:分页获取用户的 Timeline,例如获取用户 1 的前 10 篇微博
keylist = lrange user:1:mblogs 0 9
for key in keylist {
hgetall key
}题意:如果每次分页获取的微博个数较多,需要执行多次 hgetall 操作,此时可以考虑使用 pipeline(流水线)模式批量提交命令或者微博不采用哈希类型,而是使用序列化的 字符串类型,使用 mget 获取。
拆分的实现:
Pipeline (流水线):虽然咱们是多个 Redis 命令,但是把这些 命令合并成一个网络请求进行通信,大大降低客户端和服务端之间的交互次数了。
思考:
区别:Quicklist 是一种数据结构优化,而 Pipeline 是一种网络通信优化。
补充:用 list 实现栈和队列
lpush + lpop 或者 rpush + rpop 为栈。lpush + rpop 或者 rpush + lpop 为队列。