关于Redis的List 到 Stream:下一代轻量级消息队列

目录

1、订单事件

1.1、介绍

1.2、实现方式

1.3、迁移建议

2、Stream

2.1、设计目标

2.2、组成及原理

2.3、基础操作

2.4、局限性

3、实际操作

3.1、常用命令

[3.1、集成 Stream](#3.1、集成 Stream)

3.3、生产实践

[4、Redis Stream vs 其他方案](#4、Redis Stream vs 其他方案)


前沿

"我用 Redis 做消息队列好多年了 ,**不就是 LPUSH + BRPOP 吗?**Stream 是什么?有必要用吗?"

如果你也曾这样想,那你并不孤单。但在云原生时代,当系统对可靠性、可观测性、容错能力的要求越来越高时,传统的 Redis List 队列正暴露出越来越多的短板。

而 Redis Stream------这个自 2018 年 Redis 5.0 引入的原生数据类型 ------正在成为中小团队构建可靠事件驱动架构的秘密武器

如下所示:

在大多数电商/交易系统中,订单事件具有以下关键属性

订单事件 ≠ 日志流。它更像"可靠的消息队列",而非"海量数据管道"。

我们选择 Redis Stream,不是因为 Kafka 不好,而是因为:

在订单事件这个"中等吞吐、强顺序、低延迟、少消费者"的场景下,Redis Stream 以 10% 的复杂度,提供了 90% 的能力,让团队能把精力聚焦在业务创新,而非基础设施运维。

这正是 云原生时代"适度架构"(Appropriate Architecture) 的体现------不追求技术先进性,而追求交付效率与系统稳定性的最优平衡

💡 最后建议 :如果未来业务增长到 单日亿级订单 ,再考虑将核心事件链路迁移到 Kafka。

但在那之前,让简单的事情保持简单


1、订单事件

1.1、介绍

为什么 Redis 需要 Stream?

传统方案的痛点

多年来,开发者习惯用 Redis 的 List 实现简单队列:

java 复制代码
LPUSH order_queue "event"
BRPOP order_queue 0

看似完美,实则暗藏危机:

  • 消息一旦弹出即消失 → 消费者崩溃 = 消息永久丢失
  • 无 ACK 机制 → 无法实现"至少一次"投递
  • 多消费者竞争消费 → 无法保证同订单只被一个实例处理
  • 无法回溯历史 → 排查问题如盲人摸象

更糟的是,PUB/SUB 虽然支持广播,但不持久化,客户端离线即丢消息。

如下所示:

Redis 官方意识到社区需要一个"内置的、可靠的、可追溯的消息系统",而不是让每个团队自己造轮子。

于是,Redis Stream 应运而生

1.2、实现方式

如下:

特性 List(LPUSH / RPOP) Stream(XADD / XREADGROUP)
设计初衷 双端队列(Deque),用于栈/队列等基础数据结构 专为消息队列设计(类似 Kafka 轻量版)
消息 ID 无(只有值) 自动生成唯一 ID(时间戳+序列号)
消费者组 不支持 原生支持(负载均衡 + ACK + Pending List)
消息持久化 随 Redis 持久化 (更强,支持范围查询)
历史消息回溯 只能顺序消费,无法按 ID 查询 XRANGE 支持任意范围回溯
ACK 机制 无(弹出即消失) 必须显式 ACK,失败可重试
多消费者协作 竞争消费(谁抢到是谁的) 协作消费(组内负载均衡)

场景:订单创建后,通知库存服务扣减库存

方案 1:用 List(传统方式)

如下所示:

bash 复制代码
# 生产者:推送消息
LPUSH order_events '{"order_id":"1001","event":"created"}'

# 消费者:阻塞弹出
BRPOP order_events 0

问题

  • 消息被弹出后立即消失 ,如果消费者处理失败 → 消息永久丢失
  • 多个库存服务实例会竞争消费,无法保证每个实例处理不同订单
  • 无法查看历史事件(如排查"为什么没扣库存?")

List 做消息队列的致命缺陷

  1. 无 ACK 机制 → 消息易丢失
  • 消费者 BRPOP 后,消息立即从 List 移除

  • 如果消费者在处理中崩溃 → 消息永久丢失

  • 无法满足"至少一次"投递语义

  1. 无消费者组 → 无法水平扩展
  • 多个消费者同时 BRPOP,属于竞争消费(Competing Consumers)

  • 无法实现"一个订单只被一个实例处理"的语义(需额外加分布式锁)

  1. 无消息元数据 → 难以运维
  • 消息只有 value,没有 ID、时间戳、生产者信息

  • 无法追踪消息生命周期,排查问题困难

📌 真实事故案例:某电商用 List 做支付回调队列,因消费者 OOM 导致 2000+ 订单未发货,且无法重放。

**什么时候还能用 List?**虽然 Stream 是更优选择,但 List 在以下场景仍有价值:

场景 说明
瞬时任务队列 如图片缩略图生成,失败可丢弃
极简场景 单消费者、无可靠性要求
性能极致敏感 List 操作略快于 Stream(微秒级差异)

但只要涉及"业务关键事件"(如订单、支付、用户行为),必须用 Stream

方案 2:用 Stream(推荐方式)

bash 复制代码
# 生产者:写入 Stream
XADD order_events * order_id 1001 event created

# 创建消费者组(首次)
XGROUP CREATE order_events inventory_group $

# 消费者:拉取并处理
XREADGROUP GROUP inventory_group service1 COUNT 1 STREAMS order_events >
# 处理成功后
XACK order_events inventory_group 1717020800000-0

优势

  • 消息不会丢失:未 ACK 前一直存在 Pending List

  • 多个库存服务实例自动分摊消息(消费者组负载均衡)

  • 可随时 XPENDING 查看积压消息,XRANGE 回溯历史

1.3、迁移建议

从 List 到 Stream,如果你已有基于 List 的队列,可平滑迁移:

  1. 双写阶段:同时写 List 和 Stream
  2. 消费者切换:新消费者从 Stream 消费,旧消费者继续用 List
  3. 下线 List:确认无积压后停用

如下所示:

Redis 5.0+ 之后,Stream 就是 Redis 官方推荐的消息队列方案

List 应仅用于其本职工作------作为基础数据结构。


2、Stream

2.1、设计目标

Redis Stream 的设计目标非常明确:

在保持 Redis 简洁性的同时,提供接近 Kafka 的核心消息能力

具体来说,它解决了:

一句话总结:Stream 让 Redis 从"缓存数据库"升级为"轻量级事件总线"。

在日常开发中很少见到,为什么现实中很少看到 Redis Stream?

真相:不是"没人用",而是"场景匹配度决定曝光率"

  1. 历史惯性:List / PubSub 已满足简单需求
  • 很多老系统用 LPUSH + BRPOP 或 PUBLISH/SUBSCRIBE 实现了基础队列

  • "能跑就行",缺乏动力迁移(尤其无消息丢失痛点时)

  1. Kafka / RocketMQ 主导高可靠场景
  • 大厂有专职中间件团队,天然选择 Kafka

  • Redis Stream 被视为"轻量级替代",在中小团队更常见

  1. Redis Stream 是 2018 年才发布(Redis 5.0)
  • 技术普及需要时间(对比 Kafka 2011 年)

  • 很多公司 Redis 版本仍停留在 4.x(不支持 Stream)

  1. 文档与生态滞后
  • Spring Data Redis 对 Stream 的支持直到 2.2+(2019)才完善

  • 早期缺乏生产级最佳实践(如死信队列、监控)

现实情况

  • 互联网中后台系统(订单、通知、日志):Redis Stream 使用率快速上升
  • 超大规模数据管道:仍用 Kafka
  • 传统企业:可能还在用 RabbitMQ

2.2、组成及原理

如下所示:

由以下核心角色组成:

由图可知:内置消费者组,无需额外组件,支持按 ID 范围查询历史消息,消息自动持久化(随 RDB/AOF)。

如下所示:

它是一种全新的 Redis 数据类型(Data Type)

如下所示:

关键特征

  • 通过 XADD, XREAD, XGROUP 等专属命令操作
  • 内存结构为 Radix Tree + Rax(高效范围查询)
  • 持久化方式与其他类型一致(RDB/AOF)

工作原理:Stream 是如何工作的?

数据结构

  • 底层使用 Radix Tree + Rax 存储消息

  • 每条消息有唯一 ID:<毫秒时间戳>-<序列号>(如 1717020800000-0

消费者组机制

  1. 创建组 :XGROUP CREATE stream group$

  2. 拉取消息 :XREADGROUP GROUP group consumer STREAMS stream >

    • > 表示只拉新消息

    • 消息进入 Pending List

  3. 确认消息 :XACK stream group id

    • ACK 后从 Pending List 移除
  4. 处理积压:XPENDING + XCLAIM 实现死信转移

💡 这正是 Kafka Consumer Group 的简化版

2.3、基础操作

以下是一个简单的生产与消费模型:

1、创建 Stream 并写入消息

bash 复制代码
# XADD stream_name * field1 value1 field2 value2
XADD orders * order_id 1001 event_type "created" user_id "alice"
XADD orders * order_id 1002 event_type "paid" amount "99.9"

# 返回自动生成的 ID(如 1717020800000-0)
  • * 表示自动生成 ID(基于毫秒时间戳 + 序列号)
  • 也可指定 ID:XADD orders 1717020800000-0 ...

2、读取消息(简单模式)

bash 复制代码
# 读取所有消息
XRANGE orders - +  

# 读取最近 10 条
XRANGE orders - + COUNT 10

# 从某个 ID 之后读取(用于增量拉取)
XRANGE orders 1717020800000-0 + 
  • - 表示最小 ID,+ 表示最大 ID

2.4、局限性

当然,Redis Stream 并非完美,需正视其限制并制定对策:

局限 应对方案
内存存储,容量有限 - 设置 Stream 最大长度:XADD orders MAXLEN~1000000 * ... - 冷数据归档到 DB 或 Kafka(仅用于分析)
无内置 Schema 管理 - 用 JSON 格式 + 代码校验 - 或引入轻量级 Schema(如 Avro)
集群模式下 Stream 不分片 - 单 Shard 承载足够(10k QPS) - 超高吞吐时按业务分多个 Stream key(如 orders_shard_1, orders_shard_2)
消息堆积影响 Redis 性能 - 监控 XLEN orders - 消费者异常时自动告警 + 降级

3、实际操作

3.1、常用命令

如下所示:

bash 复制代码
# 写入
XADD orders * order_id 1001 event created

# 创建消费者组
XGROUP CREATE orders order_group $

# 消费
XREADGROUP GROUP order_group service1 COUNT 10 STREAMS orders >

# ACK
XACK orders order_group 1717020800000-0

# 查看积压
XPENDING orders order_group

3.1、集成 Stream

下面是一个 开箱即用 的 Spring Boot 3.x + Redis Stream 示例,包含 生产者、消费者组、ACK、异常处理

步骤 1:添加依赖(Maven)

XML 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 如果用 Lettuce(默认),无需额外依赖 -->

步骤 2:配置 Redis 连接(application.yml)

bash 复制代码
spring:
  redis:
    host: localhost
    port: 6379
    lettuce:
      pool:
        max-active: 8

步骤 3:定义事件模型

java 复制代码
public class OrderEvent {
    private String orderId;
    private String eventType;
    private String userId;
    
    // constructors, getters, setters
}

步骤 4:创建 Stream 生产者

java 复制代码
@Service
public class OrderEventProducer {
    private final RedisTemplate<String, Object> redisTemplate;

    public OrderEventProducer(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public void sendOrderEvent(OrderEvent event) {
        Map<String, String> payload = new HashMap<>();
        payload.put("orderId", event.getOrderId());
        payload.put("eventType", event.getEventType());
        payload.put("userId", event.getUserId());

        redisTemplate.opsForStream().add(
            StreamRecords.newRecord()
                .ofObject(payload)
                .withStreamKey("order_events")
        );
    }
}

步骤 5:创建 Stream 消费者(核心!)

java 复制代码
@Component
public class OrderEventConsumer implements ApplicationRunner {
    private static final String STREAM_KEY = "order_events";
    private static final String CONSUMER_GROUP = "order_group";
    private static final String CONSUMER_NAME = "inventory_service";

    private final RedisConnectionFactory connectionFactory;
    private final ObjectMapper objectMapper; // 用于 JSON 反序列化

    public OrderEventConsumer(RedisConnectionFactory connectionFactory, 
                             ObjectMapper objectMapper) {
        this.connectionFactory = connectionFactory;
        this.objectMapper = objectMapper;
    }

    @Override
    public void run(ApplicationArguments args) {
        // 1. 创建消费者组(如果不存在)
        createConsumerGroup();

        // 2. 启动消费者监听
        StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, MapRecord<String, String, String>> options =
            StreamMessageListenerContainer.StreamMessageListenerContainerOptions
                .builder()
                .pollTimeout(Duration.ofSeconds(1))
                .build();

        StreamMessageListenerContainer<String, MapRecord<String, String, String>> container =
            StreamMessageListenerContainer.create(connectionFactory, options);

        Consumer consumer = Consumer.from(CONSUMER_GROUP, CONSUMER_NAME);
        StreamOffset<String> offset = StreamOffset.create(STREAM_KEY, ReadOffset.lastConsumed());

        container.receive(consumer, offset, this::handleMessage);
        container.start();
    }

    private void handleMessage(Message<MapRecord<String, String, String>> message) {
        try {
            MapRecord<String, String, String> record = message.getRecord();
            Map<String, String> payload = record.getValue();
            
            // 转换为业务对象
            OrderEvent event = new OrderEvent();
            event.setOrderId(payload.get("orderId"));
            event.setEventType(payload.get("eventType"));
            event.setUserId(payload.get("userId"));

            // 处理业务逻辑(如扣库存)
            processOrderEvent(event);

            // 手动 ACK(关键!)
            message.acknowledge();
            
        } catch (Exception e) {
            log.error("Failed to process order event", e);
            // 不 ACK,消息会留在 Pending List,后续可重试
        }
    }

    private void createConsumerGroup() {
        try (RedisConnection connection = connectionFactory.getConnection()) {
            // XGROUP CREATE order_events order_group $ MKSTREAM
            connection.streamCommands().xGroupCreate(
                STREAM_KEY.getBytes(),
                CONSUMER_GROUP.getBytes(),
                ReadOffset.from("0-0").getOffset().getBytes(),
                true // MKSTREAM
            );
        } catch (Exception e) {
            if (!e.getMessage().contains("BUSYGROUP")) {
                throw new RuntimeException("Failed to create consumer group", e);
            }
            // BUSYGROUP 表示组已存在,忽略
        }
    }

    private void processOrderEvent(OrderEvent event) {
        // 你的业务逻辑
        System.out.println("Processing: " + event);
    }
}

步骤 6:测试发送事件

java 复制代码
@RestController
public class TestController {
    @Autowired
    private OrderEventProducer producer;

    @PostMapping("/test")
    public String test() {
        producer.sendOrderEvent(new OrderEvent("1001", "created", "user123"));
        return "Sent!";
    }
}

关键配置说明:

配置项 作用 建议值
pollTimeout 拉取消息的阻塞时间 1--5 秒
autoAcknowledge 是否自动 ACK false(必须手动控制)
ReadOffset.lastConsumed() 从上次消费位置开始 生产环境必选
MKSTREAM 自动创建 Stream 首次启动时需要

3.3、生产实践

  1. 死信队列(DLQ)
  • 消息重试 3 次后转入 order_events_dlq
  • 用 XPENDING + XCLAIM 实现
  1. 监控积压
bash 复制代码
# 查看未 ACK 消息数
XPENDING order_events order_group
  • 配合 Prometheus + Grafana 告警

3. Stream 长度限制

java 复制代码
// 发送时限制长度
redisTemplate.opsForStream().add(
    StreamRecords.newRecord()
        .ofObject(payload)
        .withStreamKey("order_events")
        .withMaxLength(1_000_000) // 保留 100 万条
);

核心要点

  • 手动 ACK 是可靠性的基石
  • 消费者组名 + 消费者名 决定消息分配
  • ReadOffset.lastConsumed() 确保断点续传
  1. 限制 Stream 长度(防内存爆炸)
bash 复制代码
# 保留最近 100 万条
XADD orders MAXLEN ~ 1000000 * order_id 1003 event_type "shipped"
  • ~ 表示"大约",性能更高(不精确删除)
  1. 监控 Pending Entries
  • 定期检查 XPENDING,避免消息堆积

  • 设置告警:XPENDING orders order_group | awk '{if($1>1000) print "ALERT"}'

  1. 消费者异常处理
  • 消费失败时不要 ACK,让消息重回 Pending List

  • 可结合 XCLAIM 实现死信队列(重试 N 次后转入 error_stream)

  1. 高可用部署
  • Redis Stream 数据随主从复制

  • 消费者组状态不复制 !故障切换后需重建消费者组(用 0 而非 $ 从头消费)


4、Redis Stream vs 其他方案

Stream vs Kafka ------ 不是替代,而是互补。

推荐方案如下:

关于stream和kafka的对比,如下所示:

前沿:Stream 的未来演进,Redis 社区并未止步:

  • Redis 7.0+ :支持 Stream 自动过期 (XADD...NOMKSTREAM)
  • Redis Stack :集成 Search & Query,可对 Stream 内容全文检索
  • Redis Streams on Flash:大容量低成本存储(企业版)

💡 趋势 :Stream 正从"消息队列"向"实时事件数据库"演进。


总结:什么时候该用 Redis Stream?

强烈推荐使用,如果你的场景满足:

  • 吞吐量 < 10w QPS

  • 需要消息不丢失、可重试

  • 团队无 Kafka 运维能力

  • 已在使用 Redis(零新增依赖)

不要使用,如果:

  • 需要百万级吞吐

  • 要求跨数据中心复制

  • 需要复杂流处理(如窗口聚合)

技术选型不是"最新最好",而是"恰到好处" 。Redis Stream 不是 Kafka 的廉价替代品,而是为特定场景量身定制的优雅解


参考文章:

1、Redis入门 - 数据类型:Stream详解

相关推荐
问道飞鱼2 小时前
【分布式知识】Redis-Shake 容器云部署完整指南
redis·分布式·redis-shake
TracyCoder1233 小时前
深入浅出Redission
redis·redission
7***99873 小时前
Redis——Windows安装
数据库·windows·redis
D***77653 小时前
【Redis】在Java中以及Spring环境下操作Redis
java·redis·spring
TracyCoder1233 小时前
Redis与MySQL数据不一致:核心场景与解决方案
数据库·redis·mysql
山水无间道4 小时前
redis的rdb文件迁移
数据库·redis·缓存
陈文锦丫4 小时前
Redis原理篇
数据库·redis·缓存
GEM的左耳返4 小时前
Java面试实战:从Spring Boot到AI集成的技术深度挑战
spring boot·redis·微服务·kafka·java面试·spring ai·缓存优化
就叫飞六吧5 小时前
Redis 安装为 Windows 服务开机后台自启
数据库·windows·redis