7、Redis消息队列
7.1 Redis消息队列-认识消息队列
什么是消息队列:字面意思就是存放消息的队列。最简单的消息队列模型包括3个角色:
-
消息队列:存储和管理消息,也被称为消息代理(Message Broker)
-
生产者:发送消息到消息队列
-
消费者:从消息队列获取消息并处理消息
使用队列的好处在于 解耦:所谓解耦,举一个生活中的例子就是:快递员(生产者)把快递放到快递柜里边(Message Queue)去,我们(消费者)从快递柜里边去拿东西,这就是一个异步,如果耦合,那么这个快递员相当于直接把快递交给你,这事固然好,但是万一你不在家,那么快递员就会一直等你,这就浪费了快递员的时间,所以这种思想在我们日常开发中,是非常有必要的。
这种场景在我们秒杀中就变成了:我们下单之后,利用redis去进行校验下单条件,再通过队列把消息发送出去,然后再启动一个线程去消费这个消息,完成解耦,同时也加快我们的响应速度。
这里我们可以使用一些现成的mq,比如kafka,rabbitmq等等,但是呢,如果没有安装mq,我们也可以直接使用redis提供的mq方案,降低我们的部署和学习成本。
🧠 理论理解:
-
消息队列是一种异步通信机制,用来解耦生产者(发送方)和消费者(接收方)。
-
核心思想:生产者只负责把消息推入队列,不需要等待消费者处理完毕,消费者则独立、异步地从队列拉取并处理消息。
-
优势包括:
-
解耦:生产与消费分离;
-
削峰填谷:平滑流量高峰;
-
异步化:提高系统吞吐量;
-
可扩展性:支持多消费者并行扩展。
-
🏢 大厂实战理解:
-
阿里、字节等电商秒杀场景用消息队列减少库存数据库压力,提高并发承载。
-
Google、OpenAI 在大规模分布式计算任务(如大模型训练)中,任务分发和结果收集用队列保证任务调度稳定。
-
NVIDIA 在 GPU 云推理调度中,用消息队列分发算力任务,实现大规模用户请求的解耦调度。
❓ 1. Redis 用作消息队列有哪些方式?各自的优缺点?
✅ 答案:
-
方式一:基于 List
-
优点:实现简单,用
LPUSH + RPOP
或RPUSH + LPOP
模拟队列。 -
缺点:
-
无消费确认,消费失败丢失。
-
无多消费者分配。
-
无阻塞保障(需用
BLPOP
等阻塞命令,但仍存在局限)。 -
适合小型、简单、低可靠需求。
-
-
-
方式二:基于 Pub/Sub
-
优点:实时广播,多个订阅者可同时接收。
-
缺点:
-
无持久化,消息发出瞬间若订阅者掉线即丢失。
-
无消费确认、重试机制。
-
不适合任务队列,只适合通知、信号场景。
-
-
-
方式三:基于 Stream
-
优点:
-
持久化存储。
-
消费者组分配任务。
-
ACK 确认、pending-list 重试。
-
支持阻塞式读取(XREAD)。
-
-
缺点:
-
Redis 5.0 及以上版本支持,老版本无法用。
-
单机性能有限,无法替代 Kafka、RocketMQ 的大规模场景。
-
-
🌍 场景题 1
阿里双 11 秒杀活动中,大量用户同时抢购秒杀券,后台用 Redis Stream 作为秒杀下单队列。问题是活动高峰时,pending-list 迅速积压,导致大量消息无法及时处理,部分用户长时间未收到订单确认。
问:如何优化?
✅ 实现方案:
-
增加消费者数量,采用消费者组机制分摊压力。
-
为消费者引入超时重试:定时扫描 pending-list,用
XCLAIM
把长时间未处理的消息转移给健康节点。 -
优化消费者处理逻辑:减少业务处理时长,必要时引入批量消费(一次读多条消息)。
-
对下游数据库增加限流或分库分表,避免单库写入瓶颈。
-
增加监控告警:pending-list 长度、消息滞留时间、ACK 成功率。
7.2 Redis消息队列-基于List实现消息队列
基于List结构模拟消息队列
消息队列(Message Queue),字面意思就是存放消息的队列。而Redis的list数据结构是一个双向链表,很容易模拟出队列效果。
队列是入口和出口不在一边,因此我们可以利用:LPUSH 结合 RPOP、或者 RPUSH 结合 LPOP来实现。 不过要注意的是,当队列中没有消息时RPOP或LPOP操作会返回null,并不像JVM的阻塞队列那样会阻塞并等待消息。因此这里应该使用BRPOP或者BLPOP来实现阻塞效果。
基于List的消息队列有哪些优缺点? 优点:
-
利用Redis存储,不受限于JVM内存上限
-
基于Redis的持久化机制,数据安全性有保证
-
可以满足消息有序性
缺点:
-
无法避免消息丢失
-
只支持单消费者
🧠 理论理解:
-
使用 Redis List 可以模拟队列(先进先出、出入分离),用
LPUSH + RPOP
或RPUSH + LPOP
实现。 -
为实现阻塞式消费,推荐使用
BLPOP
、BRPOP
,避免空轮询。 -
优缺点:
-
优点:实现简单、零成本上手。
-
缺点:无持久化、无消费确认、无多消费者分配、无失败重试。
-
🏢 大厂实战理解:
-
小规模内部工具或数据清洗任务中,大厂工程师有时用 Redis List 轻量模拟队列。
-
字节内部测试系统偶尔用 List 搭配
BLPOP
进行简易任务触发,但绝不会在核心生产系统中用它。
❓ 2. 如果 Redis Stream 出现 pending-list 积压,如何分析和解决?
✅ 答案:
-
分析步骤:
-
查看消费者组状态:使用
XPENDING
命令检查 pending-list。 -
确定是否有消费者挂掉:是否某个消费者长时间未 ACK。
-
确认业务逻辑是否处理超时:是否消费逻辑耗时异常。
-
确认是否有消息重复阻塞:是否单条消息长时间卡住。
-
-
解决方案:
-
使用
XCLAIM
命令将长时间未 ACK 的消息转移给其他健康消费者。 -
优化业务逻辑,减少消费耗时。
-
增加消费者并行度,分散处理压力。
-
设置合理的消费超时时间,避免单点阻塞。
-
7.3 Redis消息队列-基于PubSub的消息队列
PubSub(发布订阅)是Redis2.0版本引入的消息传递模型。顾名思义,消费者可以订阅一个或多个channel,生产者向对应channel发送消息后,所有订阅者都能收到相关消息。
SUBSCRIBE channel [channel] :订阅一个或多个频道 PUBLISH channel msg :向一个频道发送消息 PSUBSCRIBE pattern[pattern] :订阅与pattern格式匹配的所有频道
基于PubSub的消息队列有哪些优缺点? 优点:
-
采用发布订阅模型,支持多生产、多消费
缺点:
-
不支持数据持久化
-
无法避免消息丢失
-
消息堆积有上限,超出时数据丢失
🧠 理论理解:
-
Pub/Sub(发布订阅模型):生产者发布消息到频道,订阅者实时收到消息。
-
优缺点:
-
优点:实时推送、支持一对多广播。
-
缺点:消息无存储、断线即丢、无重试机制、不适合需要可靠性的任务。
-
🏢 大厂实战理解:
-
字节推荐系统的实时特征同步,内部组件间用 Pub/Sub 做通知,但仅用于非关键、容错场景。
-
Google Cloud 内部服务也用类似发布订阅机制传递实时状态或推送信号,但核心事务一定有其他保障(如 Kafka、Pub/Sub GCP 服务)。
❓ 3. 与 Kafka、RabbitMQ 比,Redis Stream 有什么适用场景和局限?
✅ 答案:
-
适用场景:
-
中小规模异步任务处理。
-
高频但对持久性要求中等的业务(如秒杀、活动下单)。
-
无法引入外部 MQ(部署成本、学习成本高)时,直接用 Redis 内建功能。
-
-
局限:
-
单节点 Redis 的水平扩展有限,无法像 Kafka 般支持 TB 级吞吐。
-
高可用性需依赖主从架构,但主从延迟和漂移风险比 Kafka 大。
-
运维、监控工具不如成熟 MQ 完善。
-
面试延伸点: 大厂面试官可能会追问:你会如何做 Redis Stream + Kafka 双层缓冲?(回答:先用 Redis 做内存层去抖峰,再批量推入 Kafka,既稳又高效。)
🌍 场景题 2
字节跳动的推荐系统中,用 Redis Stream 管理推荐任务队列,每个推荐模块(视频、图文、评论)作为一个消费者组处理对应任务。突然发现某个组的任务延迟急剧上升,影响推荐准确性。
问:如何定位并解决?
✅ 实现方案:
-
用
XPENDING
命令查看该组的 pending-list,确认是否有消费者挂掉或处理卡死。 -
用
XINFO CONSUMERS
查看各消费者的处理速率和积压情况。 -
短期方案:快速拉起更多消费者节点,均摊压力。
-
中长期方案:分析推荐模块业务逻辑,优化慢查询、减少依赖外部接口、分片存储任务。
-
加强任务优先级管理,优先消费核心高权重任务,减少低优先级任务堆积。
7.4 Redis消息队列-基于Stream的消息队列
Stream 是 Redis 5.0 引入的一种新数据类型,可以实现一个功能非常完善的消息队列。
发送消息的命令:
例如:
读取消息的方式之一:XREAD
例如,使用XREAD读取第一个消息:
XREAD阻塞方式,读取最新的消息:
在业务开发中,我们可以循环的调用XREAD阻塞方式来查询最新消息,从而实现持续监听队列的效果,伪代码如下
注意:当我们指定起始ID为$时,代表读取最新的消息,如果我们处理一条消息的过程中,又有超过1条以上的消息到达队列,则下次获取时也只能获取到最新的一条,会出现漏读消息的问题
STREAM类型消息队列的XREAD命令特点:
-
消息可回溯
-
一个消息可以被多个消费者读取
-
可以阻塞读取
-
有消息漏读的风险
🧠 理论理解:
-
Stream 是 Redis 专门设计的强功能消息队列,支持:
-
消息 ID 顺序化;
-
持久化存储;
-
消费确认与重试(ACK、pending-list);
-
多消费者分组、分发。
-
-
是对 List、PubSub 的全面升级。
🏢 大厂实战理解:
-
阿里和字节近年在一些低延迟业务上用 Redis Stream 替代部分 MQ 功能(如 RocketMQ、Kafka)。
-
NVIDIA 云算力任务调度系统中用 Stream 做异步任务分发,保障高并发时的可靠传输。
-
Google Cloud 的部分中间件服务,用类似 Redis Stream 的机制管理多租户事件队列。
❓ 4. 说说 Redis Stream 的 ACK 和 pending-list 机制。为什么需要它们?
✅ 答案:
-
ACK 机制:
-
消费者从 Stream 获取消息后,需要显式
XACK
确认,表示处理完成。 -
确保即使消费者挂掉,消息也不会丢失。
-
-
pending-list:
-
存放消费者已取出但未 ACK 的消息。
-
管理者或其他消费者可用
XPENDING
查看、XCLAIM
夺回重新处理。
-
-
必要性:
-
在高可靠场景下,确保即使消费者异常、挂掉或宕机,消息不会丢失。
-
支持 “至少一次” 消费语义。
-
🌍 场景题 3
Google Cloud 内部调度系统基于 Redis Stream 管理多租户 GPU 任务。最近发现当一个大租户提交数万任务时,其他租户的小任务被严重饿死,迟迟无法分配算力。
问:如何优化队列调度公平性?
✅ 实现方案:
-
为不同租户分配独立 Stream 队列,避免单队列大租户“吞噬”所有资源。
-
实施分布式限流(如每个租户按 CPU/GPU 配额分配消费速率)。
-
在 Stream 消费者逻辑中引入多队列轮询机制,确保小租户任务能被公平调度。
-
为重要租户设置高优先级队列,使用优先队列机制调整消费权重。
-
建立任务隔离池,防止大租户任务拖垮整个集群。
7.5 Redis消息队列-基于Stream的消息队列-消费者组
消费者组(Consumer Group):将多个消费者划分到一个组中,监听同一个队列。具备下列特点:
创建消费者组:
key:队列名称 groupName:消费者组名称 ID:起始ID标示,$代表队列中最后一个消息,0则代表队列中第一个消息 MKSTREAM:队列不存在时自动创建队列 其它常见命令:
删除指定的消费者组
XGROUP DESTORY key groupName
给指定的消费者组添加消费者
XGROUP CREATECONSUMER key groupname consumername
删除消费者组中的指定消费者
XGROUP DELCONSUMER key groupname consumername
从消费者组读取消息:
XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] ID [ID ...]
-
group:消费组名称
-
consumer:消费者名称,如果消费者不存在,会自动创建一个消费者
-
count:本次查询的最大数量
-
BLOCK milliseconds:当没有消息时最长等待时间
-
NOACK:无需手动ACK,获取到消息后自动确认
-
STREAMS key:指定队列名称
-
ID:获取消息的起始ID:
">":从下一个未消费的消息开始 其它:根据指定id从pending-list中获取已消费但未确认的消息,例如0,是从pending-list中的第一个消息开始
消费者监听消息的基本思路:
STREAM类型消息队列的XREADGROUP命令特点:
-
消息可回溯
-
可以多消费者争抢消息,加快消费速度
-
可以阻塞读取
-
没有消息漏读的风险
-
有消息确认机制,保证消息至少被消费一次
最后我们来个小对比
🧠 理论理解:
-
消费者组允许多个消费者分工协作消费同一队列,每条消息只被组内一个消费者处理。
-
支持:
-
多消费者分摊负载;
-
消费确认(ACK)机制;
-
未处理消息可从 pending-list 重试。
-
🏢 大厂实战理解:
-
在阿里、字节多数据中心架构中,消费者组是保证任务稳定消费的核心。
-
Google 的大规模 Cloud Task 系统类似机制用于调度跨区域的任务执行。
-
OpenAI 多 GPU 任务调度用分布式消费者组保证大模型推理任务唯一分发。
❓ 5. 实际中如何设计高可靠、高性能的秒杀下单队列系统?
✅ 答案:
-
方案设计:
-
前端请求 → Redis Stream 队列。
-
后台消费:
-
多个消费者分组分流。
-
每个消费者专注于小批量处理,快速 ACK。
-
异步落库,或落库前先缓存在本地。
-
-
-
优化点:
-
消息体尽量精简,仅存必要下单信息(如 userId、voucherId)。
-
异步批量写库,减少数据库压力。
-
设置合理重试策略,避免无限循环卡死。
-
增加监控报警,pending-list 长度超限及时拉醒。
-
-
大厂参考:
-
阿里大促活动用多级 MQ(Redis → Kafka → RocketMQ)缓冲。
-
字节用 Redis Stream 做热点防抖,配合内网 Kafka 保证一致性。
-
Google、OpenAI 跨集群任务调度用分布式协调器(如 etcd)配合内部队列。
-
🌍 场景题 4
OpenAI 分布式推理平台中,推理任务通过 Redis Stream 分发到各 GPU 节点。某次部署中,开发团队发现 Stream 中消息有重复消费问题,导致某些推理任务被重复执行,消耗大量算力。
问:如何排查和修正?
✅ 实现方案:
-
确认是否是业务代码未正确
XACK
,导致消息未确认重复消费。 -
检查消费者崩溃或超时重启后是否引入
XCLAIM
误操作。 -
增加业务侧幂等设计:即使同一任务被多次提交,实际只执行一次(如用全局任务 ID 去重)。
-
设置合理的消费者超时和
XREADGROUP
参数,避免短时间内重复拉取同一消息。 -
增强监控:记录每个任务的消费 ID、处理状态、最终 ACK 情况,发现异常自动告警。
7.6 基于Redis的Stream结构作为消息队列,实现异步秒杀下单
需求:
-
创建一个Stream类型的消息队列,名为stream.orders
-
修改之前的秒杀下单Lua脚本,在认定有抢购资格后,直接向stream.orders中添加消息,内容包含voucherId、userId、orderId
-
项目启动时,开启一个线程任务,尝试获取stream.orders中的消息,完成下单\
修改lua表达式,新增3.6
VoucherOrderServiceImpl
private class VoucherOrderHandler implements Runnable {
@Override
public void run() {
while (true) {
try {
// 1.获取消息队列中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 >
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
Consumer.from("g1", "c1"),
StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
StreamOffset.create("stream.orders", ReadOffset.lastConsumed())
);
// 2.判断订单信息是否为空
if (list == null || list.isEmpty()) {
// 如果为null,说明没有消息,继续下一次循环
continue;
}
// 解析数据
MapRecord<String, Object, Object> record = list.get(0);
Map<Object, Object> value = record.getValue();
VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
// 3.创建订单
createVoucherOrder(voucherOrder);
// 4.确认消息 XACK
stringRedisTemplate.opsForStream().acknowledge("s1", "g1", record.getId());
} catch (Exception e) {
log.error("处理订单异常", e);
//处理异常消息
handlePendingList();
}
}
}
private void handlePendingList() {
while (true) {
try {
// 1.获取pending-list中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 0
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
Consumer.from("g1", "c1"),
StreamReadOptions.empty().count(1),
StreamOffset.create("stream.orders", ReadOffset.from("0"))
);
// 2.判断订单信息是否为空
if (list == null || list.isEmpty()) {
// 如果为null,说明没有异常消息,结束循环
break;
}
// 解析数据
MapRecord<String, Object, Object> record = list.get(0);
Map<Object, Object> value = record.getValue();
VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
// 3.创建订单
createVoucherOrder(voucherOrder);
// 4.确认消息 XACK
stringRedisTemplate.opsForStream().acknowledge("s1", "g1", record.getId());
} catch (Exception e) {
log.error("处理pendding订单异常", e);
try{
Thread.sleep(20);
}catch(Exception e){
e.printStackTrace();
}
}
}
}
}
🧠 理论理解:
-
核心优化策略:
-
前端提交下单请求;
-
后端快速用 Lua 脚本校验资格并入队(Stream);
-
后台线程异步拉取消息,逐条处理、下单、ACK;
-
出错时从 pending-list 重试。
-
-
优势:
-
异步解耦、极致提速;
-
消息可靠、持久化、可追溯;
-
简化前端压力、优化用户体验。
-
🏢 大厂实战理解:
-
字节秒杀业务用异步 MQ(Kafka、Redis Stream)解耦前后端,下单成功率、系统抗压力大幅提升。
-
阿里在大促活动中,大量用 Redis Stream 配合 RocketMQ 实现多级缓冲。
-
NVIDIA 云推理 API 用 Stream 级别调度器,确保 GPU 资源高效调度、不重复分配。
❓ 6. Redis Stream 实现的异步秒杀会有哪些常见问题?如何避免?
✅ 答案:
-
常见问题:
-
pending-list 积压:未 ACK 消息太多。
-
消息丢失:极端条件下消费者重启或异常未能处理。
-
消息重复:XACK 未成功,但任务已落库。
-
消费者单点:只有一个消费者,容易被卡死。
-
-
预防方案:
-
持续监控 pending-list,定期用
XCLAIM
补偿。 -
业务层防重,幂等设计。
-
多消费者分流,提升并发能力。
-
定时保存消费位点,增强故障恢复。
-
🌍 场景题 5
NVIDIA 多机多卡大模型训练平台,用 Redis Stream 处理跨节点同步信号。某次节点升级后发现,部分同步信号漏读,导致部分训练卡住,集群算力浪费。
问:如何优化同步信号消费机制?
✅ 实现方案:
-
确保各节点消费者在订阅前已存在消费者组,并正确从
>
(最新消息)或具体 ID 开始消费。 -
防止消费者重启后错过 pending-list,用
XPENDING
和XCLAIM
补偿未确认的历史信号。 -
设置双重保障:除 Redis Stream 外,引入 fallback 信号通道(如 etcd、Zookeeper)确保关键同步。
-
增强流量压测和兼容性测试,提前验证升级对 Redis Stream 消费的影响。
-
增加集群内心跳监测,发现信号丢失立即触发重同步。