
演进3.2 之前小 list 用 ziplist、大 list 用 linkedlist3.2 起统一为 quicklist双向链表 每节点挂一块 ziplist7.0 起节点内的 ziplist 换成 listpack。3.2 以后OBJECT ENCODING对 list 始终返回quicklist。一、List 数据结构1.1 基本特点有序元素按插入顺序排列支持下标访问下标从 0 开始支持负数-1 表示倒数第一个可重复允许相同元素两端高效左/右两端的插入、弹出都是 O(1)中间操作需要遍历是 O(N)单列表最多 2³²−1 个元素。1.2 quicklist 真实结构quicklist整个列表 ┌────────────────────────────────────────────────┐ │ head ──► node0 ◄──► node1 ◄──► node2 ◄── tail │ │ count 元素总数 │ └────────────────────────────────────────────────┘ │ │ │ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ │listpack │ │listpack │ │listpack │ │ a b c d │ │ e f g h │ │ i j k l │ ← 每个节点管理一片连续紧凑内存 └─────────┘ └─────────┘ └─────────┘关键结构体源码typedef struct quicklist { quicklistNode *head; // 头节点 quicklistNode *tail; // 尾节点 unsigned long count; // 所有节点元素总数 ... } quicklist; typedef struct quicklistNode { struct quicklistNode *prev; // 前驱 struct quicklistNode *next; // 后继 unsigned char *entry; // 指向 listpack旧版为 ziplist size_t sz; // listpack 的字节大小 unsigned int count; // 本节点元素数 ... } quicklistNode;真正负责管理一片连续空间的是quicklistNode 的 prev、next、entry外加 sz、count 字段而数组其实是每个节点里的listpack。二、常用命令2.1 添加操作1右端插入RPUSH key value [value ...]2左端插入LPUSH key value [value ...]返回插入后列表长度。多个元素按顺序依次插入注意LPUSH key a b c后列表顺序是c b a每个元素都从左边插入而RPUSH key a b c 后列表顺序是a b c127.0.0.1:6379 rpush list1 a b c (integer) 3 127.0.0.1:6379 lrange list1 0 -1 1) a 2) b 3) c 127.0.0.1:6379 flushall OK 127.0.0.1:6379 rpush key a b c 3 127.0.0.1:6379 lrange key 0 -1 1)a 2) b 3)c3在指定元素前/后插入LINSERT key BEFORE|AFTER pivot value从左向右找第一个pivot在其前/后插入 value返回值成功返回插入后的列表长度pivot 不存在返回 -1key 不存在返回 0时间复杂度 O(N)需要遍历。2.2 查找操作1获取指定范围闭区间 [start, end]LRANGE key start end超出范围时自动取有效交集不会报错支持负数下标LRANGE key 0 -1取全部复杂度 O(SN)S 为偏移量、N 为返回数量。2获取指定下标元素LINDEX key index⚠️下标越界时返回(nil)。复杂度 O(N)要沿节点链表 listpack 顺序查找。3获取长度LLEN keyO(1)quicklist 头部维护 count。2.3 删除操作1左端弹出LPOP key2右端弹出RPOP key空列表返回 nil6.2 起支持LPOP key count一次弹出多个。3删除指定元素LREM key count valuecount行为count 0从左向右最多删除 count 个与 value 相等的元素count 0从右向左最多删除 |count| 个count 0删除全部与 value 相等的元素返回实际删除的元素个数O(N)。4按范围修剪保留区间删除其余LTRIM key start end保留闭区间 [start, end] 内的元素其余全部删除。常配合 LPUSH 实现定长列表O(N)。2.4 修改操作LSET key index newValue修改指定下标元素这个命令 index 越界会报错ERR index out of range注意与 LINDEX 越界返回 nil 区分。O(N)。2.5 阻塞式弹出 BLPOP / BRPOPBLPOP key [key ...] timeout BRPOP key [key ...] timeout行为规则按参数中 key 的从左到右顺序检查对第一个非空列表执行弹出立即返回[key, 弹出的值]两个元素所有 key 都为空时timeout 0阻塞等待 timeout 秒单位秒超时返回 niltimeout 0一直阻塞直到有数据多个客户端同时阻塞在同一个 key 上时数据到达按先到先得唤醒。# 客户端 A 127.0.0.1:6379 blpop task 0 # 此时阻塞…… # 客户端 B 127.0.0.1:6379 lpush task job1 # 客户端 A 立即被唤醒并返回 1) task 2) job1阻塞到底发生在哪里重点Redis 服务器不会阻塞执行 BLPOP 时服务器发现列表为空就把这个客户端挂到该 key 的阻塞等待链表上然后继续处理其他客户端的命令当其他客户端对该 key 执行 LPUSH/RPUSH 时服务器检查阻塞链表主动唤醒等待者并把数据推给它阻塞只体现在客户端连接上没有响应返回客户端等待。三、quicklist 的内部编码细节3.1 编码结论3.2 以后所有 list 统一使用 quicklist7.0 起 quicklist 节点的容器由 ziplist 变为 listpack消除连锁更新风险OBJECT ENCODING key返回quicklist。3.2 两个关键配置# 每个 quicklist 节点的大小限制 list-max-listpack-size -2 # 旧版叫 list-max-ziplist-size # 两端各保留多少个节点不压缩 list-compress-depth 0list-max-listpack-size 取值取值含义正数 n单个节点最多存 n 个元素-1单个节点最大 4KB-2默认最大 8KB-3 / -4 / -516KB / 32KB / 64KBlist-compress-depthLZF 压缩0不压缩默认1两端各留 1 个节点不压缩中间节点用 LZF 压缩2两端各留 2 个不压缩以此类推。两端节点不压缩是因为两端操作最频繁中间节点访问少压缩以牺牲少量 CPU 换取内存节省。3.3 时间复杂度汇总命令复杂度LPUSH / RPUSH / LPOP / RPOP / BLPOP / BRPOPO(1)LLENO(1)LINSERT / LREM / LTRIM / LINDEX / LSETO(N)LRANGEO(SN)四、使用场景4.1 四种经典组合组合效果LPUSH LPOPStack栈同端进出后进先出 LIFOLPUSH RPOPQueue队列异端进出先进先出 FIFOLPUSH LTRIMCapped Collection定长集合只保留最新 N 条LPUSH BRPOPMessage Queue阻塞消息队列定长列表示例只保留最新 10 条动态LPUSH feeds user:1:newpost LTRIM feeds 0 94.2 阻塞消息队列生产者LPUSH task job消费者BRPOP task 0阻塞等待天然实现任务分发。局限需要知道边界消息一旦 POP 就从 Redis 删除没有 ACK 确认机制消费者处理到一半宕机消息就丢了没有消费组、广播、消息回溯等能力复杂可靠的消息流应使用 Redis 5.0 的Stream或专业 MQ。4.3 与 Hash 配合文章列表Hash 存文章正文每个对象一个 hash多对象的同一类属性hset article:1 title xxx timestamp xxx content xxx hset article:k title yyy timestamp yyy content yyyList 存文章 ID 顺序lpush user:1:articles article:1 article:2查询时两级查找先从 list 按时间顺序取出文章 ID再用 ID 去 hash 里取正文lrange user:1:articles 0 9 # 先取最新 10 篇的 ID hgetall article:1 # 再取正文