黑马点评 Redis 消息队列二:List 和 PubSub 为什么都不是秒杀下单的最终答案?
本文继续整理黑马点评 Redis 实战篇第 7 章「Redis 消息队列」。
上一篇讲了为什么要从 JVM BlockingQueue 升级到 Redis 消息队列。
这一篇按讲义顺序看两个中间方案:Redis List 和 Redis PubSub。它们都能传消息,但为什么最后没有成为秒杀异步下单的最终方案?
1. 这篇文章解决什么问题
Redis 里有很多数据结构。
学到消息队列时,最容易想到两个方案:
text
1. List:它本来就像队列,可以左进右出。
2. PubSub:它本来就是发布订阅,可以发消息给订阅者。
那问题来了:
text
既然 List 和 PubSub 都能传消息,为什么第 7 章最后还要用 Stream?
先给结论:
List 更像一个简单队列,能阻塞读取,也能借助 Redis 存储突破 JVM 内存限制,但缺少完善的消费者组和消息确认机制;PubSub 更像广播通知,消费者在线才能收到消息,不适合订单这种不能丢的关键业务。秒杀下单需要的是可持久、可阻塞、多消费者、可确认、可恢复的队列能力,所以最终更适合用 Stream。
2. List 为什么能模拟消息队列
Redis List 是一个双向链表。
它可以从左边插入,也可以从右边弹出:
redis
LPUSH queue msg1
RPOP queue
或者反过来:
redis
RPUSH queue msg1
LPOP queue
这样就能模拟队列:
text
一端进,另一端出。
比如生产者执行:
redis
LPUSH order.queue "order-1"
LPUSH order.queue "order-2"
LPUSH order.queue "order-3"
消费者执行:
redis
RPOP order.queue
就可以按队列顺序取消息。
List 队列模型
#mermaid-svg-0yXmkzbxlFPtV7iD{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0yXmkzbxlFPtV7iD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0yXmkzbxlFPtV7iD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0yXmkzbxlFPtV7iD .error-icon{fill:#552222;}#mermaid-svg-0yXmkzbxlFPtV7iD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0yXmkzbxlFPtV7iD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0yXmkzbxlFPtV7iD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0yXmkzbxlFPtV7iD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0yXmkzbxlFPtV7iD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0yXmkzbxlFPtV7iD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0yXmkzbxlFPtV7iD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0yXmkzbxlFPtV7iD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0yXmkzbxlFPtV7iD .marker.cross{stroke:#333333;}#mermaid-svg-0yXmkzbxlFPtV7iD svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0yXmkzbxlFPtV7iD p{margin:0;}#mermaid-svg-0yXmkzbxlFPtV7iD .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-0yXmkzbxlFPtV7iD .cluster-label text{fill:#333;}#mermaid-svg-0yXmkzbxlFPtV7iD .cluster-label span{color:#333;}#mermaid-svg-0yXmkzbxlFPtV7iD .cluster-label span p{background-color:transparent;}#mermaid-svg-0yXmkzbxlFPtV7iD .label text,#mermaid-svg-0yXmkzbxlFPtV7iD span{fill:#333;color:#333;}#mermaid-svg-0yXmkzbxlFPtV7iD .node rect,#mermaid-svg-0yXmkzbxlFPtV7iD .node circle,#mermaid-svg-0yXmkzbxlFPtV7iD .node ellipse,#mermaid-svg-0yXmkzbxlFPtV7iD .node polygon,#mermaid-svg-0yXmkzbxlFPtV7iD .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0yXmkzbxlFPtV7iD .rough-node .label text,#mermaid-svg-0yXmkzbxlFPtV7iD .node .label text,#mermaid-svg-0yXmkzbxlFPtV7iD .image-shape .label,#mermaid-svg-0yXmkzbxlFPtV7iD .icon-shape .label{text-anchor:middle;}#mermaid-svg-0yXmkzbxlFPtV7iD .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0yXmkzbxlFPtV7iD .rough-node .label,#mermaid-svg-0yXmkzbxlFPtV7iD .node .label,#mermaid-svg-0yXmkzbxlFPtV7iD .image-shape .label,#mermaid-svg-0yXmkzbxlFPtV7iD .icon-shape .label{text-align:center;}#mermaid-svg-0yXmkzbxlFPtV7iD .node.clickable{cursor:pointer;}#mermaid-svg-0yXmkzbxlFPtV7iD .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0yXmkzbxlFPtV7iD .arrowheadPath{fill:#333333;}#mermaid-svg-0yXmkzbxlFPtV7iD .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0yXmkzbxlFPtV7iD .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0yXmkzbxlFPtV7iD .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0yXmkzbxlFPtV7iD .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0yXmkzbxlFPtV7iD .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0yXmkzbxlFPtV7iD .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0yXmkzbxlFPtV7iD .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0yXmkzbxlFPtV7iD .cluster text{fill:#333;}#mermaid-svg-0yXmkzbxlFPtV7iD .cluster span{color:#333;}#mermaid-svg-0yXmkzbxlFPtV7iD div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-0yXmkzbxlFPtV7iD .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0yXmkzbxlFPtV7iD rect.text{fill:none;stroke-width:0;}#mermaid-svg-0yXmkzbxlFPtV7iD .icon-shape,#mermaid-svg-0yXmkzbxlFPtV7iD .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0yXmkzbxlFPtV7iD .icon-shape p,#mermaid-svg-0yXmkzbxlFPtV7iD .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0yXmkzbxlFPtV7iD .icon-shape .label rect,#mermaid-svg-0yXmkzbxlFPtV7iD .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0yXmkzbxlFPtV7iD .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0yXmkzbxlFPtV7iD .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0yXmkzbxlFPtV7iD :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 生产者 LPUSH
Redis List: order.queue
消费者 RPOP
List 用来模拟队列非常直观。
这也是它在讲义里先出现的原因。
3. 为什么要用 BRPOP / BLPOP
如果消费者使用普通的 RPOP:
redis
RPOP order.queue
当队列里没有消息时,它会直接返回 null。
这会带来一个问题。
消费者如果想持续监听队列,就可能写成:
text
while true:
RPOP queue
如果为空就继续循环
这样会变成空转。
队列没消息时,消费者线程会一直问 Redis:
text
有消息吗?
有消息吗?
有消息吗?
这会浪费 CPU 和 Redis 请求资源。
所以 Redis 提供了阻塞式弹出命令:
redis
BRPOP queue timeout
BLPOP queue timeout
B 可以理解为 blocking。
也就是:
text
队列没消息时,不要立刻返回 null,而是阻塞等待一会儿。
这和第 6 章 BlockingQueue 的 take() 很像。
RPOP 和 BRPOP 对比
Redis List 消费者 Redis List 消费者 #mermaid-svg-3CNpdaagF0EhCopM{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-3CNpdaagF0EhCopM .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3CNpdaagF0EhCopM .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3CNpdaagF0EhCopM .error-icon{fill:#552222;}#mermaid-svg-3CNpdaagF0EhCopM .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3CNpdaagF0EhCopM .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3CNpdaagF0EhCopM .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3CNpdaagF0EhCopM .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3CNpdaagF0EhCopM .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3CNpdaagF0EhCopM .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3CNpdaagF0EhCopM .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3CNpdaagF0EhCopM .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3CNpdaagF0EhCopM .marker.cross{stroke:#333333;}#mermaid-svg-3CNpdaagF0EhCopM svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3CNpdaagF0EhCopM p{margin:0;}#mermaid-svg-3CNpdaagF0EhCopM .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-3CNpdaagF0EhCopM text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-3CNpdaagF0EhCopM .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-3CNpdaagF0EhCopM .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-3CNpdaagF0EhCopM .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-3CNpdaagF0EhCopM .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-3CNpdaagF0EhCopM #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-3CNpdaagF0EhCopM .sequenceNumber{fill:white;}#mermaid-svg-3CNpdaagF0EhCopM #sequencenumber{fill:#333;}#mermaid-svg-3CNpdaagF0EhCopM #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-3CNpdaagF0EhCopM .messageText{fill:#333;stroke:none;}#mermaid-svg-3CNpdaagF0EhCopM .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-3CNpdaagF0EhCopM .labelText,#mermaid-svg-3CNpdaagF0EhCopM .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-3CNpdaagF0EhCopM .loopText,#mermaid-svg-3CNpdaagF0EhCopM .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-3CNpdaagF0EhCopM .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-3CNpdaagF0EhCopM .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-3CNpdaagF0EhCopM .noteText,#mermaid-svg-3CNpdaagF0EhCopM .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-3CNpdaagF0EhCopM .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-3CNpdaagF0EhCopM .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-3CNpdaagF0EhCopM .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-3CNpdaagF0EhCopM .actorPopupMenu{position:absolute;}#mermaid-svg-3CNpdaagF0EhCopM .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-3CNpdaagF0EhCopM .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-3CNpdaagF0EhCopM .actor-man circle,#mermaid-svg-3CNpdaagF0EhCopM line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-3CNpdaagF0EhCopM :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} RPOP queue队列为空,立即返回 nullBRPOP queue 5队列为空,最多阻塞等待 5 秒
4. List 方案比 BlockingQueue 好在哪里
List 至少解决了第 6 章 BlockingQueue 的一部分问题。
好处 1:不受 JVM 内存限制
BlockingQueue 在 Java 进程内存中。
Redis List 在 Redis 中。
订单任务不再堆在某个 Tomcat 的 JVM 内存里。
好处 2:Redis 有持久化机制
Redis 可以配置 RDB 或 AOF。
虽然 Redis 持久化也不是绝对不丢,但比 JVM 内存队列可靠得多。
好处 3:多个 Java 实例可以访问同一个队列
多台服务都可以:
text
往同一个 Redis List 写消息
从同一个 Redis List 读消息
这比每台服务一个本地 BlockingQueue 更适合集群。
5. List 方案为什么仍然不够
List 最大的问题是:
text
它只是一个简单队列,不是完整消息队列。
讲义里总结它的缺点:
text
1. 无法避免消息丢失。
2. 只支持单消费者。
这两点需要展开讲。
问题 1:消息被取走后,处理失败怎么办
假设消费者执行:
redis
BRPOP order.queue 5
取到一条订单消息:
text
order-1
注意,BRPOP 取消息的同时,会把消息从 List 中删除。
然后消费者开始处理:
text
扣数据库库存
保存订单
如果消费者刚取出消息就宕机了:
text
消息已经从 Redis List 删除
数据库订单还没保存
这条消息就没了。
这就是消息丢失风险。
问题 2:缺少确认机制
真正的消息队列通常需要:
text
消费者处理成功后,再确认消息。
也就是 ACK 机制。
List 的基本弹出模型是:
text
取出消息 = 删除消息
它没有天然的:
text
先投递给消费者
消费者处理成功后再确认删除
处理失败还能恢复
这种完整机制。
问题 3:消费者组能力弱
秒杀下单可能希望多个消费者一起处理订单消息。
List 可以让多个消费者都 BRPOP 同一个队列,但它没有像 Stream 消费者组那样清晰记录:
text
这条消息被哪个消费者拿了
是否已经 ACK
未确认消息在哪里
失败后怎么重新处理
所以 List 适合简单任务队列,不适合订单这种关键业务的最终方案。
6. PubSub 是什么
PubSub 是 Redis 的发布订阅模型。
它包含:
text
发布者:向 channel 发送消息
订阅者:订阅 channel,接收消息
channel:频道
常见命令:
redis
SUBSCRIBE channel
PUBLISH channel message
PSUBSCRIBE pattern
比如消费者订阅:
redis
SUBSCRIBE order.channel
生产者发布:
redis
PUBLISH order.channel "order-1"
订阅了 order.channel 的消费者就能收到消息。
PubSub 模型
#mermaid-svg-xxDCfzpUFbKjlcfY{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xxDCfzpUFbKjlcfY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xxDCfzpUFbKjlcfY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xxDCfzpUFbKjlcfY .error-icon{fill:#552222;}#mermaid-svg-xxDCfzpUFbKjlcfY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xxDCfzpUFbKjlcfY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xxDCfzpUFbKjlcfY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xxDCfzpUFbKjlcfY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xxDCfzpUFbKjlcfY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xxDCfzpUFbKjlcfY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xxDCfzpUFbKjlcfY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xxDCfzpUFbKjlcfY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xxDCfzpUFbKjlcfY .marker.cross{stroke:#333333;}#mermaid-svg-xxDCfzpUFbKjlcfY svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xxDCfzpUFbKjlcfY p{margin:0;}#mermaid-svg-xxDCfzpUFbKjlcfY .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-xxDCfzpUFbKjlcfY .cluster-label text{fill:#333;}#mermaid-svg-xxDCfzpUFbKjlcfY .cluster-label span{color:#333;}#mermaid-svg-xxDCfzpUFbKjlcfY .cluster-label span p{background-color:transparent;}#mermaid-svg-xxDCfzpUFbKjlcfY .label text,#mermaid-svg-xxDCfzpUFbKjlcfY span{fill:#333;color:#333;}#mermaid-svg-xxDCfzpUFbKjlcfY .node rect,#mermaid-svg-xxDCfzpUFbKjlcfY .node circle,#mermaid-svg-xxDCfzpUFbKjlcfY .node ellipse,#mermaid-svg-xxDCfzpUFbKjlcfY .node polygon,#mermaid-svg-xxDCfzpUFbKjlcfY .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xxDCfzpUFbKjlcfY .rough-node .label text,#mermaid-svg-xxDCfzpUFbKjlcfY .node .label text,#mermaid-svg-xxDCfzpUFbKjlcfY .image-shape .label,#mermaid-svg-xxDCfzpUFbKjlcfY .icon-shape .label{text-anchor:middle;}#mermaid-svg-xxDCfzpUFbKjlcfY .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xxDCfzpUFbKjlcfY .rough-node .label,#mermaid-svg-xxDCfzpUFbKjlcfY .node .label,#mermaid-svg-xxDCfzpUFbKjlcfY .image-shape .label,#mermaid-svg-xxDCfzpUFbKjlcfY .icon-shape .label{text-align:center;}#mermaid-svg-xxDCfzpUFbKjlcfY .node.clickable{cursor:pointer;}#mermaid-svg-xxDCfzpUFbKjlcfY .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xxDCfzpUFbKjlcfY .arrowheadPath{fill:#333333;}#mermaid-svg-xxDCfzpUFbKjlcfY .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xxDCfzpUFbKjlcfY .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xxDCfzpUFbKjlcfY .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xxDCfzpUFbKjlcfY .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xxDCfzpUFbKjlcfY .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xxDCfzpUFbKjlcfY .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xxDCfzpUFbKjlcfY .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xxDCfzpUFbKjlcfY .cluster text{fill:#333;}#mermaid-svg-xxDCfzpUFbKjlcfY .cluster span{color:#333;}#mermaid-svg-xxDCfzpUFbKjlcfY div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xxDCfzpUFbKjlcfY .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xxDCfzpUFbKjlcfY rect.text{fill:none;stroke-width:0;}#mermaid-svg-xxDCfzpUFbKjlcfY .icon-shape,#mermaid-svg-xxDCfzpUFbKjlcfY .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xxDCfzpUFbKjlcfY .icon-shape p,#mermaid-svg-xxDCfzpUFbKjlcfY .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xxDCfzpUFbKjlcfY .icon-shape .label rect,#mermaid-svg-xxDCfzpUFbKjlcfY .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xxDCfzpUFbKjlcfY .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xxDCfzpUFbKjlcfY .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xxDCfzpUFbKjlcfY :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 生产者 PUBLISH
channel: order.channel
订阅者 1
订阅者 2
订阅者 3
PubSub 的特点是:
text
一条消息可以被多个订阅者收到。
这和 List 的"一个消息被一个消费者取走"不一样。
7. PubSub 适合什么,不适合什么
PubSub 更像广播。
它适合这种场景:
text
通知在线用户
广播配置变更
实时推送某个事件
这些场景通常强调:
text
在线就收到,不在线就算了。
但订单业务不一样。
订单消息不能说:
text
消费者当时不在线,所以这单就不处理了。
订单消息必须尽量可靠。
这就是 PubSub 不适合秒杀下单的根本原因。
8. PubSub 的主要问题
讲义里总结 PubSub 的缺点:
text
1. 不支持数据持久化。
2. 无法避免消息丢失。
3. 消息堆积有上限,超出时数据丢失。
展开看。
问题 1:消费者不在线会丢消息
如果生产者发布消息时,消费者没有订阅或断线了,这条消息不会被保存下来等消费者回来。
它更像:
text
广播喊了一声,谁在线谁听见。
不在线的人不会补听。
问题 2:没有消息确认机制
PubSub 不关心消费者是否真的处理成功。
生产者发布后,Redis 把消息推给订阅者。
至于订阅者处理成功还是失败,PubSub 模型本身不负责。
问题 3:消息堆积能力弱
如果消费者处理不过来,消息不能像专业 MQ 那样稳定堆积。
超过一定限制后可能丢失。
对订单业务来说,这个风险很高。
9. List、PubSub、订单业务的适配度
可以这样理解:
text
List:像一个简单任务队列。
PubSub:像一个广播通知系统。
秒杀订单:需要可靠任务队列。
订单消息需要的能力包括:
text
1. 消息能保存下来。
2. 消费者可以阻塞等待。
3. 多个消费者可以一起处理,提高速度。
4. 消费成功后要确认。
5. 消费失败后还能找回来重试。
List 只能满足一部分。
PubSub 更不适合关键订单。
Stream 才是更贴近这些需求的 Redis 数据结构。
对比图
#mermaid-svg-dM297yWhNwdICF2a{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dM297yWhNwdICF2a .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dM297yWhNwdICF2a .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dM297yWhNwdICF2a .error-icon{fill:#552222;}#mermaid-svg-dM297yWhNwdICF2a .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dM297yWhNwdICF2a .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dM297yWhNwdICF2a .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dM297yWhNwdICF2a .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dM297yWhNwdICF2a .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dM297yWhNwdICF2a .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dM297yWhNwdICF2a .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dM297yWhNwdICF2a .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dM297yWhNwdICF2a .marker.cross{stroke:#333333;}#mermaid-svg-dM297yWhNwdICF2a svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dM297yWhNwdICF2a p{margin:0;}#mermaid-svg-dM297yWhNwdICF2a .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-dM297yWhNwdICF2a .cluster-label text{fill:#333;}#mermaid-svg-dM297yWhNwdICF2a .cluster-label span{color:#333;}#mermaid-svg-dM297yWhNwdICF2a .cluster-label span p{background-color:transparent;}#mermaid-svg-dM297yWhNwdICF2a .label text,#mermaid-svg-dM297yWhNwdICF2a span{fill:#333;color:#333;}#mermaid-svg-dM297yWhNwdICF2a .node rect,#mermaid-svg-dM297yWhNwdICF2a .node circle,#mermaid-svg-dM297yWhNwdICF2a .node ellipse,#mermaid-svg-dM297yWhNwdICF2a .node polygon,#mermaid-svg-dM297yWhNwdICF2a .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dM297yWhNwdICF2a .rough-node .label text,#mermaid-svg-dM297yWhNwdICF2a .node .label text,#mermaid-svg-dM297yWhNwdICF2a .image-shape .label,#mermaid-svg-dM297yWhNwdICF2a .icon-shape .label{text-anchor:middle;}#mermaid-svg-dM297yWhNwdICF2a .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-dM297yWhNwdICF2a .rough-node .label,#mermaid-svg-dM297yWhNwdICF2a .node .label,#mermaid-svg-dM297yWhNwdICF2a .image-shape .label,#mermaid-svg-dM297yWhNwdICF2a .icon-shape .label{text-align:center;}#mermaid-svg-dM297yWhNwdICF2a .node.clickable{cursor:pointer;}#mermaid-svg-dM297yWhNwdICF2a .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-dM297yWhNwdICF2a .arrowheadPath{fill:#333333;}#mermaid-svg-dM297yWhNwdICF2a .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dM297yWhNwdICF2a .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dM297yWhNwdICF2a .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dM297yWhNwdICF2a .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-dM297yWhNwdICF2a .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dM297yWhNwdICF2a .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-dM297yWhNwdICF2a .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dM297yWhNwdICF2a .cluster text{fill:#333;}#mermaid-svg-dM297yWhNwdICF2a .cluster span{color:#333;}#mermaid-svg-dM297yWhNwdICF2a div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-dM297yWhNwdICF2a .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-dM297yWhNwdICF2a rect.text{fill:none;stroke-width:0;}#mermaid-svg-dM297yWhNwdICF2a .icon-shape,#mermaid-svg-dM297yWhNwdICF2a .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dM297yWhNwdICF2a .icon-shape p,#mermaid-svg-dM297yWhNwdICF2a .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-dM297yWhNwdICF2a .icon-shape .label rect,#mermaid-svg-dM297yWhNwdICF2a .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dM297yWhNwdICF2a .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-dM297yWhNwdICF2a .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-dM297yWhNwdICF2a :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 不足
不足
不足
不足
不足
不足
秒杀订单消息需求
要保存消息
要支持阻塞读取
要支持多个消费者协作
要支持 ACK
失败后要能恢复
List
PubSub
Stream
10. 为什么讲义还要讲 List 和 PubSub
既然最后不用 List 和 PubSub,为什么还要讲?
因为它们帮助我们理解:
text
一个消息队列到底需要哪些能力。
List 告诉我们:
text
能存消息、能阻塞读还不够,还要处理确认和失败恢复。
PubSub 告诉我们:
text
能发布给多个消费者也不够,关键业务还需要持久化和可靠性。
最后 Stream 出场时,我们才能明白它强在哪里。
如果一上来就背 Stream 命令,很容易变成:
text
只记得 XADD、XREADGROUP、XACK,
但不知道它们分别在补哪个短板。
这也是这章按照讲义顺序学的价值。
11. 本篇最容易混淆的几个点
1. List 能不能做消息队列
能做简单消息队列。
但它不是完整可靠消息队列。
它适合简单任务,不太适合订单这种必须尽量可靠处理的业务。
2. BRPOP 阻塞读取是不是就等于消息可靠
不是。
阻塞读取只解决"没消息时不要空转"。
它不解决"取出消息后消费者处理失败怎么办"。
3. PubSub 支持多消费者,是不是比 List 更适合订单
不是。
PubSub 的多消费者是广播模型。
但它不持久化,消费者离线会丢消息,也没有 ACK。
订单业务不能接受"当时没在线就错过"。
4. 为什么最终用 Stream
因为 Stream 兼顾:
text
消息持久化
阻塞读取
多消费者协作
消息确认
pending-list 异常恢复
这些能力更适合异步秒杀下单。
12. 面试怎么回答
如果面试官问:Redis List 能不能实现消息队列,有什么问题?
可以这样回答:
Redis List 可以通过
LPUSH + RPOP或RPUSH + LPOP模拟队列,也可以通过BRPOP/BLPOP实现阻塞读取。它相比 JVM BlockingQueue 的好处是消息存在 Redis 中,不受单个 JVM 内存限制,并且可以利用 Redis 持久化。但 List 的基本弹出语义是取出即删除,缺少完善的 ACK 和 pending-list 机制,消费者取到消息后如果处理失败,消息可能丢失,因此不适合作为关键订单业务的最终方案。
如果面试官问:PubSub 为什么不适合秒杀订单?
可以这样回答:
PubSub 是发布订阅模型,适合广播通知。生产者向 channel 发布消息,在线订阅者可以收到。但 PubSub 不持久化消息,消费者不在线时会丢消息,也没有消息确认和失败恢复机制。秒杀订单消息必须可靠处理,不能因为消费者掉线就丢,所以 PubSub 不适合作为秒杀下单队列。
13. 总结
这一篇按讲义顺序看了两个 Redis MQ 的中间方案:
text
List:
能模拟队列,能阻塞读取,比 JVM 队列更进一步,
但缺少完善 ACK 和失败恢复。
PubSub:
能发布订阅,支持广播给多个消费者,
但不持久化,消费者离线会丢消息。
所以第 7 章最后要继续走向 Stream。
记住这一句:
秒杀订单消息不是普通通知,它必须尽量可靠地被消费、确认、失败恢复;这就是 List 和 PubSub 不够、Stream 要登场的原因。
下一篇继续讲:
text
Stream 和消费者组到底强在哪里?XADD、XREADGROUP、ACK、pending-list 分别解决什么问题?