黑马程序猿 - Redis - 实战篇 - 7.Redis消息队列

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 + RPOPRPUSH + 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 + RPOPRPUSH + LPOP 实现。

  • 为实现阻塞式消费,推荐使用 BLPOPBRPOP,避免空轮询。

  • 优缺点

    • 优点:实现简单、零成本上手。

    • 缺点:无持久化、无消费确认、无多消费者分配、无失败重试。

🏢 大厂实战理解:

  • 小规模内部工具或数据清洗任务中,大厂工程师有时用 Redis List 轻量模拟队列。

  • 字节内部测试系统偶尔用 List 搭配 BLPOP 进行简易任务触发,但绝不会在核心生产系统中用它。

 

❓ 2. 如果 Redis Stream 出现 pending-list 积压,如何分析和解决?

✅ 答案:

  • 分析步骤:

    1. 查看消费者组状态:使用 XPENDING 命令检查 pending-list。

    2. 确定是否有消费者挂掉:是否某个消费者长时间未 ACK。

    3. 确认业务逻辑是否处理超时:是否消费逻辑耗时异常。

    4. 确认是否有消息重复阻塞:是否单条消息长时间卡住。

  • 解决方案:

    • 使用 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. 实际中如何设计高可靠、高性能的秒杀下单队列系统?

✅ 答案:

  • 方案设计:

    1. 前端请求 → Redis Stream 队列。

    2. 后台消费:

      • 多个消费者分组分流。

      • 每个消费者专注于小批量处理,快速 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();
                }
            }
        }
    }
}

🧠 理论理解:

  • 核心优化策略:

    1. 前端提交下单请求;

    2. 后端快速用 Lua 脚本校验资格并入队(Stream);

    3. 后台线程异步拉取消息,逐条处理、下单、ACK;

    4. 出错时从 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,用 XPENDINGXCLAIM 补偿未确认的历史信号。

  • 设置双重保障:除 Redis Stream 外,引入 fallback 信号通道(如 etcd、Zookeeper)确保关键同步。

  • 增强流量压测和兼容性测试,提前验证升级对 Redis Stream 消费的影响。

  • 增加集群内心跳监测,发现信号丢失立即触发重同步。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夏驰和徐策

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值