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 更轻量

七、结语

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

相关推荐
java1234_小锋2 小时前
Java高频面试题:说说Redis的内存淘汰策略?
java·开发语言·redis
GDAL2 小时前
SQLite 与 MySQL 性能深度对比:场景决定最优解
数据库·mysql·sqlite
troublea2 小时前
Laravel 8.x新特性全解析
数据库·mysql·缓存
焦糖玛奇朵婷2 小时前
做盲盒小程序,如何少走弯路?
数据库·程序人生·小程序·开源软件·软件需求
mi20062 小时前
Linux下安装postgresql记录
数据库·postgresql
清云随笔2 小时前
MySQL 的常见操作(基础)
数据库·mysql
汇智信科2 小时前
汇智信科网络考试系统:以技术赋能,重构在线测评新范式
linux·数据库·mysql·oracle·sqlserver·java技术
BullSmall2 小时前
从2026年春晚 详细分析未来IT行业的发展
linux·运维·服务器·数据库