Redis消息队列-基于Stream的消息队列

一、前言:为什么 Redis Stream 是"专业级"消息队列?

在 Redis 5.0 之前,开发者只能用 ListPub/Sub 实现简单消息传递,但它们存在明显短板:

  • List:无 ACK、无法重试、不支持多消费者协同
  • Pub/Sub:消息不持久、离线即丢

Redis Stream 的出现,彻底改变了这一局面!

它具备:

✅ 消息持久化

✅ 消费者组(Consumer Group)

✅ ACK 确认机制

✅ 消息回溯与重放

✅ 多播 + 负载均衡

本文将带你从零实现一个基于 Redis Stream 的高可靠异步任务系统,并对比其与 List/PubSub 的核心差异。


二、Stream 核心概念速览

概念 说明
Stream 类似 Kafka 的日志,每条消息有唯一 ID(如 1698765432123-0
Consumer Group 消费者组,组内多个消费者竞争消费 ,组间广播消费
Pending Entries 已读但未 ACK 的消息列表,支持失败重试
ACK 消费成功后确认,避免重复消费

📌 一句话理解
Stream = Redis 版的 Kafka(轻量级)


三、核心命令演示(Redis CLI)

1. 创建消息流

bash 复制代码
# XADD stream_name * field value
> XADD order_stream * event "create_order" user_id "1001"
"1698765432123-0"

2. 创建消费者组

bash 复制代码
# XGROUP CREATE stream group_name id [MKSTREAM]
> XGROUP CREATE order_stream order_group 0 MKSTREAM
OK

3. 消费者读取消息(组内)

bash 复制代码
# XREADGROUP GROUP group consumer COUNT n STREAMS stream >
> XREADGROUP GROUP order_group worker1 COUNT 1 STREAMS order_stream >
1) 1) "order_stream"
   2) 1) 1) "1698765432123-0"
         2) 1) "event"
            2) "create_order"
            3) "user_id"
            4) "1001"

4. 确认处理完成(ACK)

bash 复制代码
# XACK stream group id
> XACK order_stream order_group 1698765432123-0
(integer) 1

5. 查看未确认消息(Pending)

bash 复制代码
> XPENDING order_stream order_group
1) (integer) 0     # 未 ACK 消息数
2) "1698765432123-0"
3) "1698765432123-0"
4) 1) 1) "worker1"
      2) "1"       # 此消费者有 1 条 pending

四、Spring Boot 完整实战

步骤 1:添加依赖(Spring Data Redis ≥ 2.2)

XML 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

💡 确保 Redis 版本 ≥ 5.0


步骤 2:定义任务模型

java 复制代码
public class OrderTask {
    private Long orderId;
    private Long userId;
    private String requestId; // 幂等 ID
    // getters/setters
}

步骤 3:封装 Stream 操作工具类

java 复制代码
@Component
public class RedisStreamUtil {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 发送消息到 Stream
     */
    public String sendMessage(String streamKey, Map<String, String> message) {
        return redisTemplate.opsForStream().add(
            StreamRecords.newRecord()
                .ofObject(message)
                .withStreamKey(streamKey)
        ).getValue();
    }

    /**
     * 从消费者组读取消息
     */
    public List<MapRecord<String, String, String>> readMessages(
            String streamKey, String groupName, String consumerName, int count) {
        return redisTemplate.opsForStream().read(
            Consumer.from(groupName, consumerName),
            StreamReadOptions.empty().count(count).block(Duration.ofSeconds(1)),
            StreamOffset.create(streamKey.getBytes(), ReadOffset.lastConsumed())
        );
    }

    /**
     * 确认消息已处理
     */
    public void ackMessage(String streamKey, String groupName, String... messageIds) {
        redisTemplate.opsForStream().acknowledge(groupName, streamKey, messageIds);
    }
}

步骤 4:启动消费者线程(支持 ACK + 重试)

java 复制代码
@Component
public class OrderTaskConsumer {

    @Autowired
    private RedisStreamUtil streamUtil;

    private static final String STREAM_KEY = "order_task_stream";
    private static final String GROUP_NAME = "order_group";

    @PostConstruct
    public void startConsumer() {
        // 创建消费者组(幂等)
        createGroupIfNotExists();

        Thread consumer = new Thread(this::consumeLoop);
        consumer.setDaemon(true);
        consumer.setName("stream-order-consumer");
        consumer.start();
    }

    private void createGroupIfNotExists() {
        try {
            redisTemplate.execute((RedisCallback<Void>) connection -> {
                connection.streamCommands().xGroupCreate(
                    STREAM_KEY.getBytes(),
                    GROUP_NAME.getBytes(),
                    ReadOffset.from("0-0"),
                    true // MKSTREAM
                );
                return null;
            });
        } catch (Exception e) {
            if (!e.getMessage().contains("BUSYGROUP")) {
                log.error("创建消费者组失败", e);
            }
        }
    }

    private void consumeLoop() {
        while (!Thread.currentThread().isInterrupted()) {
            try {
                List<MapRecord<String, String, String>> records =
                    streamUtil.readMessages(STREAM_KEY, GROUP_NAME, "worker-1", 10);

                for (MapRecord<String, String, String> record : records) {
                    try {
                        // 解析消息
                        Map<String, String> data = record.getValue();
                        OrderTask task = parseTask(data);

                        // 处理业务
                        processOrder(task);

                        // ACK 确认
                        streamUtil.ackMessage(STREAM_KEY, GROUP_NAME, record.getId().getValue());

                    } catch (Exception e) {
                        log.error("处理消息失败, id={}", record.getId(), e);
                        // 不 ACK,消息会留在 Pending,后续可重试
                    }
                }

                if (records.isEmpty()) {
                    Thread.sleep(500); // 避免空轮询
                }

            } catch (Exception e) {
                log.error("消费异常", e);
                try { Thread.sleep(1000); } catch (InterruptedException ie) { break; }
            }
        }
    }

    private void processOrder(OrderTask task) {
        // 扣库存、创建订单、发通知...
        orderService.createOrder(task.getOrderId(), task.getUserId());
    }
}

步骤 5:生产者发送任务

java 复制代码
@Service
public class OrderService {

    @Autowired
    private RedisStreamUtil streamUtil;

    public void submitOrder(Long orderId, Long userId) {
        Map<String, String> message = Map.of(
            "orderId", orderId.toString(),
            "userId", userId.toString(),
            "requestId", UUID.randomUUID().toString()
        );

        String messageId = streamUtil.sendMessage("order_task_stream", message);
        log.info("订单任务已提交, messageId={}", messageId);
    }
}

五、Stream 的核心优势总结

特性 说明
持久化 消息写入内存+磁盘,重启不丢
ACK 机制 消费成功才删除,失败可重试
消费者组 支持水平扩展,自动负载均衡
消息回溯 可按 ID 查询历史消息(XRANGE
积压监控 XPENDING 查看未处理消息

真正实现了"至少一次"语义(At-Least-Once Delivery)


六、适用场景 vs 不适用场景

✅ 推荐使用:

  • 订单异步创建
  • 优惠券发放
  • 日志收集
  • 用户行为分析
  • 需要可靠处理的后台任务

❌ 不推荐:

  • 超高吞吐(> 10万 QPS)→ 选 Kafka
  • 复杂路由/事务 → 选 RabbitMQ
  • 仅需广播 → 用 Pub/Sub 更轻量

七、结语

感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!

相关推荐
Arya_aa8 分钟前
Mysql数据库-管理和存储数据库(开源管理系统)与JDBC操作数据库步骤,JUnit以及如何将压缩包中exe程序添加上桌面图标
数据库·mysql·junit·开源
最懒的菜鸟1 小时前
redis缓存击穿
数据库·redis·缓存
ok_hahaha1 小时前
java从头开始-苍穹外卖day05-Redis及店铺营业状态设置
java·开发语言·redis
qq_404265831 小时前
用Python批量处理Excel和CSV文件
jvm·数据库·python
人间打气筒(Ada)2 小时前
mysql数据库之DDL、DML
运维·数据库·sql·mysql·dba·dml·dql
代码派2 小时前
信创迁移“不敢切”的最后一公里:数据一致性校验怎么做才算够?
数据库·数据库开发·dba·etl工程师·数据库管理工具·信创数据库·信创迁移
qq_418101772 小时前
使用Scikit-learn进行机器学习模型评估
jvm·数据库·python
熙胤3 小时前
PostgreSQL 向量扩展插件pgvector安装和使用
数据库·postgresql
牢七3 小时前
baijiacms-master 审计
数据库
数据知道3 小时前
MongoDB聚合管道性能优化:阶段重排与内存使用控制策略
数据库·mongodb·性能优化