引言
上一篇我们吃透了 String 类型,这篇接着讲第二种类型------**List(列表)**。如果说 String 是"一个 Key 存一个值",那 List 就是"一个 Key 存一串有序的值",非常适合表达"按顺序排列的一组数据"。
List 在 Redis 里有个特别亮眼的用途:消息队列。很多中小项目不想引入 Kafka、RocketMQ 这么重的东西,就干脆用 Redis 的 List 做轻量级队列。这篇文章我会把 List 的底层结构、命令、内部编码和应用场景一次讲清楚,最后落在一个消息队列的实战例子上。
一、List 是什么
Redis 的 List 是一个有序、可重复 的字符串列表,底层是双向链表。它的最大特点是:
- 头尾操作极快:从头部或尾部插入、弹出元素都是 O(1)。
- 中间操作较慢 :按下标随机访问(
LINDEX)是 O(n),因为它要顺着链表找。
所以 List 天生适合做"先进先出"的队列,或"后进先出"的栈,而不适合频繁按下标随机访问的场景。
二、底层结构:从 ziplist + linkedlist 到 quicklist
Redis 的 List 底层实现经过了几次演进,面试常问:
- 早期 :元素少且短时用 ziplist(压缩列表) ,元素多或长时用 linkedlist(双向链表),两者二选一。
- Redis 3.2 之后 :统一改用 quicklist 。quicklist 本质是"linkedlist + ziplist 的组合"------一个由多个 ziplist 节点串起来的链表,兼顾了链表的灵活和 ziplist 的紧凑内存。
- Redis 7.0 之后 :ziplist 又被 listpack 逐步替代(listpack 修复了 ziplist 级联更新的缺陷)。
记住一个结论即可:**现代 Redis 的 List 用 quicklist(内部是 listpack)实现,兼顾内存紧凑和头尾操作效率。**
三、常用命令详解
3.1 从两端插入 / 弹出
plaintext
127.0.0.1:6379> RPUSH mylist a b c # 从右边(尾部)插入
(integer) 3
127.0.0.1:6379> LPUSH mylist 0 # 从左边(头部)插入
(integer) 4
127.0.0.1:6379> LRANGE mylist 0 -1 # 查看全部(0 到 -1 表示从头到尾)
1) "0"
2) "a"
3) "b"
4) "c"
127.0.0.1:6379> LPOP mylist # 从左边弹出
"0"
127.0.0.1:6379> RPOP mylist # 从右边弹出
"c"
3.2 查看与修改
plaintext
127.0.0.1:6379> LLEN mylist # 列表长度
(integer) 2
127.0.0.1:6379> LINDEX mylist 0 # 按下标取元素(O(n))
"a"
127.0.0.1:6379> LSET mylist 0 A # 按下标修改
OK
127.0.0.1:6379> LRANGE mylist 0 -1
1) "A"
2) "b"
3.3 插入与删除指定元素
plaintext
127.0.0.1:6379> RPUSH mylist x y x
(integer) 5
127.0.0.1:6379> LREM mylist 2 x # 从头删除 2 个 x
(integer) 2
127.0.0.1:6379> LINSERT mylist BEFORE y new # 在 y 前面插入 new
(integer) 6
3.4 截取(保留区间,其余删除)
plaintext
127.0.0.1:6379> RPUSH news 1 2 3 4 5 6 7 8 9 10
(integer) 10
127.0.0.1:6379> LTRIM news 0 4 # 只保留前 5 个
OK
127.0.0.1:6379> LRANGE news 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
LTRIM 是实现"最新 N 条"的利器:每插入一条就 trim 一下,列表永远只留最新的一批。
3.5 阻塞式弹出(消息队列核心)
plaintext
# 客户端 A:BLPOP 阻塞等待,超时时间 0 表示一直等
127.0.0.1:6379> BLPOP queue 0
# ... 此时会阻塞,直到有元素进来 ...
# 客户端 B:往队列里推一条
127.0.0.1:6379> LPUSH queue hello
(integer) 1
# 客户端 A 立即收到
1) "queue"
2) "hello"
BLPOP / BRPOP 是阻塞命令:队列为空时会一直等待,有元素时立即弹出。这避免了"轮询空队列"白白消耗 CPU 的问题,是 Redis 做消息队列的关键。
3.6 移动元素(可靠队列基础)
plaintext
127.0.0.1:6379> RPUSH source 1 2 3
(integer) 3
127.0.0.1:6379> RPOPLPUSH source backup # 从 source 尾部弹出,塞到 backup 头部
"3"
RPOPLPUSH(新版本叫 LMOVE)常用于实现"可靠队列":消费时先把消息转移到备份队列,处理完再删除,这样消费者崩溃时消息不会丢。
四、内部编码速查
| 编码 | 说明 |
|---|---|
| quicklist | 3.2+ 的默认实现,linkedlist + listpack 的组合 |
| (旧)ziplist | 早期小列表用,已逐步被 listpack 取代 |
| (旧)linkedlist | 早期大列表用 |
用 OBJECT ENCODING 可以查看:
plaintext
127.0.0.1:6379> OBJECT ENCODING mylist
"listpack"
五、典型应用场景
5.1 消息队列(轻量级)
用 LPUSH + BRPOP 实现最简单的队列:
java
// 生产者:往队列里放消息
public void produce(StringRedisTemplate redis, String message) {
redis.opsForList().leftPush("order:queue", message);
}
// 消费者:阻塞式取消息
public String consume(StringRedisTemplate redis) {
// rightPop(key, timeout, unit) 队列为空时阻塞等待
return redis.opsForList().rightPop("order:queue", 0, TimeUnit.SECONDS);
}
适合小规模、对可靠性要求不高 的场景。要注意:Redis 的 List 队列是"消费即删除",如果消费者处理消息时崩溃,消息就丢了;需要可靠性的场景要用 RPOPLPUSH 做备份,或直接上专业的 MQ。
5.2 最新消息 / 最新评论列表
用 LPUSH 新消息进来 + LTRIM 只保留最新 N 条:
java
public void addLatest(StringRedisTemplate redis, String news) {
String key = "latest:news";
redis.opsForList().leftPush(key, news); // 新消息从头部进
redis.opsForList().trim(key, 0, 99); // 只保留最新 100 条
}
public List<String> getLatest(StringRedisTemplate redis) {
return redis.opsForList().range("latest:news", 0, -1);
}
微博、新闻 App 的"最新动态"列表就是这么做的。
5.3 栈与普通队列
- 栈(后进先出) :
LPUSH+LPOP(同侧进出)。 - 队列(先进先出) :
LPUSH+RPOP(异侧进出)。
java
// 栈
redis.opsForList().leftPush("stack", "a");
String top = redis.opsForList().leftPop("stack");
// 队列
redis.opsForList().leftPush("queue", "a");
String head = redis.opsForList().rightPop("queue");
5.4 简单的优先级/任务缓冲
配合多个队列(如 task:high、task:normal),消费者优先 BRPOP 高优先级队列,取不到再去低优先级队列,就能实现简单的任务分级。
六、命令速查表
| 命令 | 作用 |
|---|---|
| LPUSH / RPUSH | 从左 / 右插入 |
| LPOP / RPOP | 从左 / 右弹出 |
| BLPOP / BRPOP | 阻塞式弹出 |
| LRANGE | 按区间取元素 |
| LLEN | 列表长度 |
| LINDEX | 按下标取元素 |
| LSET | 按下标修改 |
| LINSERT | 在指定元素前后插入 |
| LREM | 删除指定元素 |
| LTRIM | 截取保留区间 |
| RPOPLPUSH / LMOVE | 移动元素到另一个列表 |
七、总结
Redis 的 List 是一个有序、可重复的双向链表结构,要点如下:
- 底层用 quicklist(内部 listpack),头尾操作 O(1)、中间随机访问 O(n),所以"从两端进出"是它的强项。
- 核心命令分几类:两端插入弹出(LPUSH/RPUSH/LPOP/RPOP)、查看修改(LRANGE/LINDEX/LSET)、条件操作(LINSERT/LREM/LTRIM)、阻塞弹出(BLPOP/BRPOP)、元素移动(RPOPLPUSH/LMOVE)。
- 最典型应用是轻量级消息队列(LPUSH + BRPOP)和"最新 N 条"列表(LPUSH + LTRIM),也能轻松实现栈和队列。
- 做消息队列时要注意"消费即删除"的特性,可靠性要求高时需要
RPOPLPUSH备份或改用专业 MQ。
一句话总结:List 天生适合"从两端进出"的顺序数据------做队列、做栈、做最新列表,用它就对了。