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

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

宕机丢任务示意图

MySQL BlockingQueue Java 服务 用户 MySQL BlockingQueue Java 服务 用户 秒杀请求 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编程工具,助力开发者即刻编程。

更多推荐