Redis 消息队列

Redis 消息队列全网精讲:从原理到实战,一篇搞定面试+开发

前言

很多同学学 Redis 消息队列时都会遇到这些灵魂拷问:

  • List、PubSub、Stream 到底该选谁?
  • 为什么 Stream 是生产级首选?
  • 消费者组 ACK、Pending-List 到底解决什么问题?
  • 秒杀场景为什么要用 Stream 做异步下单?

本文不讲废话,只讲你最容易忘、面试最爱问、开发最容易踩坑的内容,结构清晰,可直接当博客发布。


一、消息队列到底解决什么问题?(必背)

1.1 三大角色

  • 生产者:发消息
  • 消息队列:存消息
  • 消费者:消费消息

1.2 核心价值(高频考点)

  1. 解耦:生产者不依赖消费者,系统更灵活
  2. 异步:无需同步等待,接口响应极快
  3. 削峰:高并发流量排队,保护数据库

秒杀场景经典用法:

Redis 校验资格 → 发消息到队列 → 异步创建订单
前端快速返回,后端慢慢消费,完美抗住高并发


二、Redis 三种消息队列对比(面试必考)

Redis 实现消息队列有三种方案,一定要记住优缺点和适用场景,这是最容易忘的点!

2.1 基于 List 实现(最原始)

原理 :利用双向链表实现 FIFO,LPUSH + BRPOP

优点

  • 支持 Redis 持久化
  • 消息有序
  • 不受 JVM 内存限制

缺点

  • 只能单消费者
  • 无法避免消息丢失
  • 不支持广播

适用场景:简单单消费者业务,不推荐生产


2.2 基于 PubSub 发布订阅(Redis 2.0)

原理:发布-订阅模型,多消费者可同时收到消息

优点

  • 支持多生产多消费
  • 实时性高

缺点

  • 不支持持久化
  • 消息极易丢失
  • 消费者离线就丢消息

适用场景:临时通知、广播,不适合核心业务


2.3 基于 Stream(Redis 5.0 生产级王者)

真正企业级消息队列,支持持久化、ACK、消费者组、回溯。

优点

  • 消息可持久化、可回溯
  • 支持消费者组负载均衡
  • 有 ACK 确认机制
  • Pending-List 保证消息至少消费一次
  • 无漏读、不丢失

缺点

  • 学习成本稍高

适用场景:秒杀、订单、支付等核心业务(秒杀标配)


2.4 三张表总结(建议收藏)

方案 持久化 多消费者 漏读 消息丢失 生产可用
List
PubSub
Stream

一句话结论:生产只推荐 Stream


三、Stream 核心知识点(最容易忘,背会涨薪)

3.1 XREAD 与 XREADGROUP 区别

XREAD

  • 多消费者都能收到同一条消息
  • 无 ACK,无 Pending-List
  • 存在漏读风险

XREADGROUP(消费者组)

  • 一条消息只被一个消费者消费
  • ACK 机制:必须手动确认
  • Pending-List:保存未 ACK 消息,异常可重试
  • 无漏读、负载均衡

高频面试题:为什么消费者组要 ACK?

答:保证消息至少被消费一次,避免业务异常导致消息丢失。

3.2 关键符号含义(极易忘)

  • >:读取下一条未消费的新消息
  • 0:从 Pending-List 读取未 ACK 的历史消息
  • $:读取最新消息(XREAD 使用,会漏读)

3.3 核心命令

bash 复制代码
# 创建队列+消费者组
XGROUP CREATE stream.orders g1 0 MKSTREAM

# 消费者组消费
XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS stream.orders >

# 确认消息
XACK stream.orders g1 消息ID

四、秒杀场景实战:Stream 异步下单(面试必问)

4.1 为什么要用 Stream 做秒杀?

  1. Redis 抗高并发,Lua 原子判断库存+一人一单
  2. 异步削峰,避免数据库被打崩
  3. 消息不丢失、不重复、可重试
  4. 完美契合秒杀业务模型

4.2 流程(背会直接讲给面试官)

  1. Lua 脚本判断库存、用户资格
  2. 校验通过 → XADD 写入 Stream
  3. 后台线程使用消费者组监听消息
  4. 消费 → 创建订单 → ACK
  5. 异常时重试 Pending-List 消息

4.3 核心消费代码(生产可用)

java 复制代码
@Slf4j
private class VoucherOrderHandler implements Runnable {
    @Override
    public void run() {
        while (true) {
            try {
                // 1. 读取新消息
                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())
                );
                if (list == null || list.isEmpty()) continue;

                // 2. 解析消息
                MapRecord<String, Object, Object> record = list.get(0);
                VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(record.getValue(), new VoucherOrder(), true);

                // 3. 创建订单(事务+幂等)
                createVoucherOrder(voucherOrder);

                // 4. ACK 确认
                stringRedisTemplate.opsForStream().acknowledge("stream.orders", "g1", record.getId());
            } catch (Exception e) {
                log.error("订单处理异常", e);
                handlePendingList();
            }
        }
    }

    // 处理未ACK消息,保证不丢失
    private void handlePendingList() {
        while (true) {
            try {
                List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
                    Consumer.from("g1", "c1"),
                    StreamReadOptions.empty().count(1),
                    StreamOffset.create("stream.orders", ReadOffset.from("0"))
                );
                if (list == null || list.isEmpty()) break;

                MapRecord<String, Object, Object> record = list.get(0);
                VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(record.getValue(), new VoucherOrder(), true);
                createVoucherOrder(voucherOrder);
                stringRedisTemplate.opsForStream().acknowledge("stream.orders", "g1", record.getId());
            } catch (Exception e) {
                try { Thread.sleep(20); } catch (InterruptedException ignored) {}
            }
        }
    }
}

五、高频疑问解答

Q1:消费者组为什么必须处理 Pending-List?

答:消费者崩溃、网络异常会导致消息未 ACK,这些消息会留在 Pending-List,不处理就相当于消息丢失,所以必须循环重试。

Q2:XREAD 为什么会漏读?

答:使用 $ 只读最新消息,如果中间堆积多条,只会读最后一条,导致消息丢失。

Q3:Stream 可以替代 Kafka 吗?

答:简单业务可以,高可靠性、事务消息、回溯查询等复杂场景还是 Kafka 更强。

Q4:秒杀为什么不用阻塞队列 ArrayBlockingQueue?

答:单机可行,集群环境下消息不同步,Stream 是分布式消息队列,适合集群部署。


六、总结(看完这一段就够复习)

  1. Redis 消息队列三种方案:List、PubSub、Stream
  2. 只有 Stream 适合生产,支持 ACK、消费者组、Pending-List
  3. 消费者组 = 负载均衡 + 消息可靠
  4. ACK + Pending-List = 保证消息至少消费一次
  5. 秒杀架构标配:Lua + Stream + 异步消费

本文覆盖 Redis 消息队列所有高频考点+易忘点+实战坑点,无论是面试还是开发,收藏这一篇就够了!


相关推荐
小小小米粒2 小时前
Collection(单列集合)、Map(双列集合),容易搞混的 Collections 工具类。
java·开发语言
二本咕咕-机械转码2 小时前
STM32是怎么跑起来的?启动流程 + 时钟树一次讲透(面试高频)
stm32·嵌入式硬件·面试
曹牧2 小时前
Oracle:分批查询
数据库·oracle
祭曦念2 小时前
MySQL基础运维:mysqldump全量备份与恢复实操 | 新手可直接落地的备份指南
运维·数据库·mysql
skiy2 小时前
springboot+全局异常处理
java·spring boot·spring
愤豆2 小时前
07-Java语言核心-JVM原理-JVM对象模型详解
java·jvm·c#
东离与糖宝2 小时前
零基础Java学生面试通关手册:项目+算法+框架一次搞定
java·人工智能·面试
gaozhiyong08132 小时前
超越跑分:Gemini 3.1 Pro 2026年多维度能力评估体系深度拆解
java·开发语言
皙然2 小时前
深入解析Java volatile关键字:作用、底层原理与实战避坑
java·开发语言