黑马点评 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 章链路图

用户秒杀请求

请求线程执行 Lua

Redis 资格判断通过?

返回失败

VoucherOrder 放入 JVM BlockingQueue

请求线程返回订单 id

当前 JVM 后台线程

take 队列任务

MySQL 扣库存并保存订单

这张图里最重要的关键词是:

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 订单可能永远不会创建

这就是本地内存队列的致命短板。

宕机丢任务示意图

MySQLBlockingQueueJava 服务用户MySQLBlockingQueueJava 服务用户秒杀请求Lua 判断通过放入订单任务返回订单 id服务宕机队列任务丢失,无法落库

如果订单任务存到 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 队列

Redis 消息队列

Tomcat A

Redis Stream

Tomcat B

Tomcat C

消费者 1

消费者 2

本地 BlockingQueue

Tomcat A

Queue A

Tomcat B

Queue B

Tomcat C

Queue C

这就是第 7 章要做的事:

把订单任务从 JVM 本地队列,挪到 Redis 中的共享消息队列。

6. 什么是消息队列

讲义中对消息队列的定义很直白:

消息队列就是存放消息的队列。

最简单的消息队列模型包含三个角色:

1. 生产者:发送消息的人。
2. 消息队列:保存和管理消息的地方。
3. 消费者:从队列中取消息并处理的人。

放到秒杀业务里:

生产者:秒杀请求线程,或者 Lua 脚本
消息队列:Redis Stream
消费者:后台下单线程
消息:订单任务,包含 userId、voucherId、orderId

秒杀中的 MQ 模型

生产者:秒杀请求线程 / Lua

消息队列:stream.orders

消费者:后台下单线程

MySQL:扣库存并保存订单

消息队列的作用不是让业务消失。

它只是把业务从:

请求线程马上做

改成:

先发消息,后台消费者稍后做

7. 消息队列的三个核心价值

讲消息队列时,经常会听到三个词:

解耦
异步
削峰

这三个词如果只背,很空。

放回秒杀业务里就很好理解。

价值 1:解耦

没有消息队列时,请求线程必须知道:

怎么扣库存
怎么保存订单
异常怎么处理

引入消息队列后,请求线程只需要:

把订单消息发出去

后台消费者负责:

拿到消息后落库

生产者和消费者通过队列连接,不再直接调用。

这就是解耦。

价值 2:异步

没有消息队列时:

请求线程必须等数据库下单完成才能返回。

引入消息队列后:

请求线程发完消息就可以返回。
后台消费者慢慢处理数据库落库。

这就是异步。

价值 3:削峰

秒杀请求可能一瞬间进来很多。

如果所有请求都直接打数据库,数据库会被瞬时流量冲击。

消息队列可以把瞬时流量先接住:

前端瞬间来了 1 万个请求
Redis 快速判断资格并写入消息队列
后台消费者按自己的速度慢慢处理

这就是削峰。

它不是让总工作量减少。

而是把瞬时压力摊平。

三个价值示意图

高并发秒杀请求

Redis 资格判断

消息队列暂存订单消息

后台消费者按能力消费

MySQL 落库

解耦:生产者不直接调用消费者

异步:请求线程不等待落库

削峰:瞬时流量变成队列积压


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 才更接近真正消息队列。

演进路线图

BlockingQueue

Redis List

Redis PubSub

Redis Stream

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,但最终都不是秒杀下单的最佳答案?
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐