黑马点评-Redis 消息队列-01_why_redis_mq
黑马点评 Redis 消息队列一:为什么 BlockingQueue 不够,Redis 还能当 MQ?
本文整理黑马点评 Redis 实战篇第 7 章「Redis 消息队列」。
第 6 章我们已经把秒杀下单从“请求线程同步落库”优化成了“Redis 预检 + BlockingQueue 异步下单”。第 7 章继续往前走:为什么还要把本地阻塞队列换成 Redis 消息队列?
这一篇先不急着背 Redis 命令,先把“为什么需要消息队列”这件事讲清楚。
1. 这篇文章解决什么问题
学到第 7 章时,很容易产生一个疑惑:
第 6 章不是已经用 BlockingQueue 实现异步下单了吗?
请求线程已经能快速返回了,后台线程也能慢慢落库了。
那为什么第 7 章还要学习 Redis 消息队列?
这个问题非常关键。
因为第 6 章解决的是:
请求线程不要同步等待数据库落库。
第 7 章继续解决的是:
订单任务不能只放在当前 JVM 内存里。
先给结论:
BlockingQueue 可以帮助我们理解异步下单思想,但它是 JVM 本地内存队列,存在内存限制、服务宕机丢任务、多实例不共享等问题。Redis 消息队列把订单任务放到 Redis 这种独立中间件中,让多个服务实例都能生产和消费消息,并具备更好的持久化、阻塞读取、消息确认和异常恢复能力。
2. 先复习第 6 章 BlockingQueue 做了什么
第 6 章的异步秒杀流程可以简化成:
请求线程:
执行 Lua 判断库存和一人一单
↓
判断通过
↓
封装 VoucherOrder
↓
放入 BlockingQueue
↓
返回订单 id
后台线程:
从 BlockingQueue 中 take 订单任务
↓
执行数据库扣库存和保存订单
它的核心变化是:
请求线程不再直接创建数据库订单。
这当然已经比同步下单快很多。
但是 BlockingQueue 有一个隐藏前提:
队列只存在当前 Java 进程的内存里。
这个前提在学习阶段没问题,但放到更真实的高并发系统里就会暴露问题。
第 6 章链路图
这张图里最重要的关键词是:
JVM BlockingQueue
它不是独立的消息中间件。
它只是当前服务实例内存中的一个队列对象。
3. BlockingQueue 的第一个问题:内存有限
第 6 章讲义中队列类似这样:
private BlockingQueue<VoucherOrder> orderTasks =
new ArrayBlockingQueue<>(1024 * 1024);
这说明队列是有容量上限的。
如果秒杀请求非常多,Redis 预检通过的订单任务也很多,就可能出现:
队列满了
↓
新的订单任务放不进去
↓
请求线程无法正常交付任务
更关键的是:
这个容量占的是 Java 进程内存。
如果队列里堆积太多任务,JVM 内存压力会变大。
所以 BlockingQueue 的异步能力不是无限的。
它只是把请求线程和后台线程解耦了,但没有从根上解决任务存储可靠性问题。
4. BlockingQueue 的第二个问题:服务宕机会丢任务
这是订单业务里更严重的问题。
假设一个请求已经通过 Lua:
Redis 库存已经扣减
Redis 已下单 Set 已经记录 userId
订单任务已经放入 BlockingQueue
请求线程已经返回订单 id
但后台线程还没来得及落库。
这时服务进程突然宕机。
会发生什么?
BlockingQueue 在 JVM 内存里
JVM 进程没了
队列里的订单任务也没了
结果就是:
用户看起来抢到了
Redis 里也记录了资格
但 MySQL 订单可能永远不会创建
这就是本地内存队列的致命短板。
宕机丢任务示意图
如果订单任务存到 Redis、RabbitMQ、Kafka 这类独立中间件里,即使某个 Java 服务宕机,消息仍然有机会被其他消费者继续处理。
这就是消息队列从“本地队列”升级成“独立中间件”的价值。
5. BlockingQueue 的第三个问题:多实例不共享
真实项目通常不会只部署一个 Java 服务。
可能是:
Nginx
├── Tomcat A
├── Tomcat B
└── Tomcat C
如果每个 Tomcat 内部都有一个 BlockingQueue,那么它们是三份不同的队列:
Tomcat A 的 BlockingQueue
Tomcat B 的 BlockingQueue
Tomcat C 的 BlockingQueue
这些队列互相看不见。
Tomcat A 放进去的订单任务,Tomcat B 的后台线程消费不到。
这会带来几个问题:
1. 任务分散在不同 JVM 中,统一管理困难。
2. 某个实例宕机,只会丢该实例内存中的任务。
3. 后台消费能力无法天然统一调度。
而 Redis 消息队列是共享的:
Tomcat A、B、C 都往同一个 Redis Stream 写消息。
多个消费者也从同一个 Stream 读消息。
本地队列 vs Redis 队列
这就是第 7 章要做的事:
把订单任务从 JVM 本地队列,挪到 Redis 中的共享消息队列。
6. 什么是消息队列
讲义中对消息队列的定义很直白:
消息队列就是存放消息的队列。
最简单的消息队列模型包含三个角色:
1. 生产者:发送消息的人。
2. 消息队列:保存和管理消息的地方。
3. 消费者:从队列中取消息并处理的人。
放到秒杀业务里:
生产者:秒杀请求线程,或者 Lua 脚本
消息队列:Redis Stream
消费者:后台下单线程
消息:订单任务,包含 userId、voucherId、orderId
秒杀中的 MQ 模型
消息队列的作用不是让业务消失。
它只是把业务从:
请求线程马上做
改成:
先发消息,后台消费者稍后做
7. 消息队列的三个核心价值
讲消息队列时,经常会听到三个词:
解耦
异步
削峰
这三个词如果只背,很空。
放回秒杀业务里就很好理解。
价值 1:解耦
没有消息队列时,请求线程必须知道:
怎么扣库存
怎么保存订单
异常怎么处理
引入消息队列后,请求线程只需要:
把订单消息发出去
后台消费者负责:
拿到消息后落库
生产者和消费者通过队列连接,不再直接调用。
这就是解耦。
价值 2:异步
没有消息队列时:
请求线程必须等数据库下单完成才能返回。
引入消息队列后:
请求线程发完消息就可以返回。
后台消费者慢慢处理数据库落库。
这就是异步。
价值 3:削峰
秒杀请求可能一瞬间进来很多。
如果所有请求都直接打数据库,数据库会被瞬时流量冲击。
消息队列可以把瞬时流量先接住:
前端瞬间来了 1 万个请求
Redis 快速判断资格并写入消息队列
后台消费者按自己的速度慢慢处理
这就是削峰。
它不是让总工作量减少。
而是把瞬时压力摊平。
三个价值示意图
8. 为什么这里不用 Kafka、RabbitMQ,而先学 Redis MQ
真实项目里,消息队列可以用很多成熟组件:
RabbitMQ
Kafka
RocketMQ
Redis Stream
黑马点评这里选择 Redis 消息队列,有几个原因:
1. 项目里已经引入 Redis,不需要额外部署新中间件。
2. 当前业务是学习型秒杀场景,Redis Stream 足够演示消息队列核心机制。
3. Redis Stream 支持阻塞读取、消费者组、ACK、pending-list,能比 List 和 PubSub 更接近真正 MQ。
但要注意:
Redis Stream 能当消息队列用,不代表它在所有场景都能替代专业 MQ。
如果是大型业务、高可靠消息、复杂路由、海量日志流、跨系统事件治理,还是可能选择 RabbitMQ、Kafka、RocketMQ 这类专门 MQ。
黑马点评第 7 章的目标更聚焦:
在不引入新中间件的情况下,用 Redis Stream 改造第 6 章本地阻塞队列。
9. 第 7 章的演进路线
第 7 章不是一上来直接用 Stream。
讲义按这个顺序讲:
1. 认识消息队列
2. List 模拟消息队列
3. PubSub 发布订阅
4. Stream 普通读写
5. Stream 消费者组
6. Stream 改造异步秒杀下单
这个顺序是很合理的。
因为它在回答一个连续问题:
Redis 能不能当 MQ?
如果能,用哪个数据结构最合适?
List 可以当队列,但能力不完整。
PubSub 可以广播消息,但可靠性不足。
Stream 才更接近真正消息队列。
演进路线图
这一章真正要记住的是:
Redis 消息队列不是一个命令,而是一组能力的逐步补齐:存消息、阻塞读、多消费者、消息确认、异常恢复。
10. 本篇最容易混淆的几个点
1. 消息队列是不是为了让下单逻辑不执行
不是。
下单逻辑仍然要执行。
只是从请求线程执行,变成后台消费者执行。
2. BlockingQueue 算不算消息队列
从思想上算。
它也有生产者、队列、消费者。
但它是 JVM 本地内存队列,不适合作为更可靠的分布式消息队列。
3. Redis 消息队列是不是一定比专业 MQ 好
不是。
Redis Stream 是轻量方案,适合项目已经有 Redis、业务规模不复杂、希望降低部署成本的场景。
专业 MQ 在消息可靠性、生态、运维能力、复杂路由等方面通常更成熟。
4. 削峰是不是减少了数据库工作量
不是。
削峰不是减少总量,而是把瞬时高峰变成后端可承受的消费速度。
11. 面试怎么回答
如果面试官问:为什么第 6 章已经用了 BlockingQueue,第 7 章还要换成 Redis Stream?
可以这样回答:
BlockingQueue 是 JVM 本地内存队列,虽然能实现请求线程和后台线程解耦,但存在内存容量限制、服务宕机丢任务、多实例部署时队列不共享等问题。Redis Stream 是 Redis 提供的消息队列结构,消息存储在 Redis 中,多个服务实例都能访问同一个队列,并且支持阻塞读取、消费者组、ACK 和 pending-list,更适合承接异步秒杀订单任务。
如果面试官问:消息队列在秒杀业务里起什么作用?
可以这样回答:
在秒杀业务里,消息队列用于把请求线程和数据库落库线程解耦。请求线程只负责执行 Redis Lua 脚本判断库存和一人一单,判断通过后把订单消息写入队列并快速返回订单 id。后台消费者再从队列读取订单消息,执行数据库扣库存和保存订单。这样可以提高接口响应速度,并把瞬时高并发流量削成后台可控的消费速度。
12. 总结
这一篇的主线是:
第 6 章 BlockingQueue 能讲通异步思想
↓
但它是 JVM 本地内存队列
↓
存在内存限制、宕机丢任务、多实例不共享
↓
所以第 7 章引入 Redis 消息队列
↓
用 Redis Stream 承接秒杀订单消息
最重要的是记住:
消息队列不是为了让订单不落库,而是为了让请求线程不直接等落库;订单任务先进入队列,后台消费者再按自己的节奏处理。
下一篇继续看 Redis 里最容易想到的两个方案:
List 和 PubSub 为什么看起来能做 MQ,但最终都不是秒杀下单的最佳答案?
更多推荐



所有评论(0)