Redisson 操作 Redis Stream 消息队列详解及实战案例

目录

[一、Redis Stream 概念](#一、Redis Stream 概念)

1.Redis消息队列-认识消息队列

[2.Redis Stream特点](#2.Redis Stream特点)

[3.Redis Stream与RabbitMQ等消息队列的比较](#3.Redis Stream与RabbitMQ等消息队列的比较)

[二.Redis Stream基本操作命令](#二.Redis Stream基本操作命令)

1.生产消息

2.消费消息

3.消费者组操作

4.确认消息处理

[三、Redisson 操作 Stream 的核心 API](#三、Redisson 操作 Stream 的核心 API)

[1. 获取流的API](#1. 获取流的API)

[2. StreamAddArgs](#2. StreamAddArgs)

[2.1 初始方法](#2.1 初始方法)

[2.2 消息修剪策略](#2.2 消息修剪策略)

[3. 添加消息的API](#3. 添加消息的API)

[3.1 add](#3.1 add)

[3.2 addAll](#3.2 addAll)

[4. StreamMessageId](#4. StreamMessageId)

[4.1 基本结构](#4.1 基本结构)

[4.2 特殊常量](#4.2 特殊常量)

[5. 读取消息API](#5. 读取消息API)

6.消费者组操作

[6.1 创建消费者组](#6.1 创建消费者组)

[6.2 StreamReadGroupArgs](#6.2 StreamReadGroupArgs)

[6.2.1 初始方法](#6.2.1 初始方法)

[6.2.2 可选配置方法](#6.2.2 可选配置方法)

[7. 消费者组读取](#7. 消费者组读取)

8.确认消息

[9. 删除消息API](#9. 删除消息API)

[9.1 StreamTrimArgs](#9.1 StreamTrimArgs)

[9.1.1 初始方法](#9.1.1 初始方法)

[9.1.2 可选配置方法](#9.1.2 可选配置方法)

[9.2 初始方法](#9.2 初始方法)

[9.2.1 remove与trim主要区别对比](#9.2.1 remove与trim主要区别对比)

[9.2.2 trim 方法执行时机问题解析](#9.2.2 trim 方法执行时机问题解析)

[10. 获取流信息](#10. 获取流信息)

11.范围查询

[11.1 getPendingInfo](#11.1 getPendingInfo)

[11.1.1 可选配置方法](#11.1.1 可选配置方法)

[11.1.2 getPendingInfo方法演示案例](#11.1.2 getPendingInfo方法演示案例)

[11.2 range方法演示案例](#11.2 range方法演示案例)

四、完整使用案例:订单处理系统

1.依赖引入和Redis连接配置

[2. 初始化 Redisson 配置](#2. 初始化 Redisson 配置)

[3. 订单生产者服务](#3. 订单生产者服务)

[4. 订单处理消费者服务](#4. 订单处理消费者服务)

[5. 主程序测试](#5. 主程序测试)

6.运行结果

7.关键点解析

[五、Redis Stream 定向消息路由方案](#五、Redis Stream 定向消息路由方案)

1.实现方案比较

[2.多Stream分离 + 路由Key](#2.多Stream分离 + 路由Key)

[1. 架构设计](#1. 架构设计)

六、完整使用案例:订单处理反馈系统

1.路由服务实现

2.订单生产者服务

3.订单处理服务

4.订单通知服务

5.主程序测试

6.运行结果

[6.1 订单处理服务生效](#6.1 订单处理服务生效)

[6.2 订单通知服务生效](#6.2 订单通知服务生效)

7.关键点解析

[七、Redis Stream 消息清理策略](#七、Redis Stream 消息清理策略)

[1. 消费者延迟ACK消息清理策略](#1. 消费者延迟ACK消息清理策略)

[1.1 生产者服务](#1.1 生产者服务)

[1.2 消费者服务](#1.2 消费者服务)

[1.3 主程序测试](#1.3 主程序测试)

[1.4 运行结果](#1.4 运行结果)

[2. 消费者即时ACK+立即删除策略](#2. 消费者即时ACK+立即删除策略)

[2.1 消费者服务](#2.1 消费者服务)

[2.2 运行结果](#2.2 运行结果)

[3. 两种消息清理策略详细对比](#3. 两种消息清理策略详细对比)

[3.1. 核心机制对比](#3.1. 核心机制对比)

[3.2. 性能表现对比](#3.2. 性能表现对比)

[3.3. 可靠性对比](#3.3. 可靠性对比)

[3.4. 资源占用对比](#3.4. 资源占用对比)

[3.5. 典型应用场景](#3.5. 典型应用场景)


Redis Stream关于Redisson的操作代码https://download.csdn.net/download/m0_74808313/90561172

一、Redis Stream 概念

1.Redis消息队列-认识消息队列

什么是消息队列:字面意思就是存放消息的队列。最简单的消息队列模型包括3个角色:

  • 消息队列:存储和管理消息,也被称为消息代理(Message Broker)

  • 生产者:发送消息到消息队列

  • 消费者:从消息队列获取消息并处理消息

2.Redis Stream特点

Redis Stream 是 Redis 5.0 引入的专门为消息队列设计的数据结构,相比 Pub/Sub 和 List 实现的消息队列,它提供了:

  1. 消息持久化 - 消息不会因消费而立即消失

  2. 消费者组(Consumer Group) - 支持多个消费者组独立消费

  3. 消息回溯 - 可以重新消费历史消息

  4. ACK 确认机制 - 确保消息被正确处理

  5. 阻塞/非阻塞读取 - 灵活的消息获取方式

3.Redis Stream与RabbitMQ等消息队列的比较

  • Redis Stream 是轻量级解决方案,适合简单、实时的消息队列需求。

  • Kafka 是分布式流处理平台,适合大数据和高吞吐场景。

  • RabbitMQ 是通用消息代理,适合复杂路由和企业集成。

  • 总结:

    如果需要 简单、低延迟 的队列,选 Redis Stream。如果需要 复杂路由、可靠投递、企业级特性,选 RabbitMQ。

二.Redis Stream基本操作命令

1.生产消息

向 mystream 添加消息,指定键值对

XADD mystream * key1 value1 key2 value2

2.消费消息

读取最新消息(非阻塞)

XREAD COUNT 2 STREAMS mystream 0

阻塞式读取(超时时间毫秒)

XREAD BLOCK 5000 STREAMS mystream $

3.消费者组操作

创建消费者组 XGROUP CREATE mystream mygroup $ # 消费者组消费 XREADGROUP GROUP mygroup consumer1 COUNT 1 STREAMS mystream >

4.确认消息处理

XACK mystream mygroup 消息ID

三、Redisson 操作 Stream 的核心 API

Redisson 提供了 RStream 接口来操作 Stream 数据结构:

1. 获取流的API

RStream<K, V> getStream(String name) 获取流,不存在则创建
java 复制代码
// 获取Stream对象,若不存在则创建
RStream<String, String> stream = redisson.getStream("myStream");

2. StreamAddArgs

StreamAddArgs 是 Redisson 提供的一个配置类,用于定义向 Redis Stream 中添加消息时的行为。它允许你设置多种参数,包括消息内容、修剪策略、最大长度限制等。通过 StreamAddArgs,你可以灵活地控制消息的添加和流的管理。

2.1 初始方法

entries(Map<K, V> map) 将一个包含键值对的 Map(消息) 转换为 StreamAddArgs 对象

2.2 消息修剪策略

TrimStrategy 是 Redisson 中用于控制 Redis Stream 修剪行为的枚举类型,它定义了两种不同的流修剪策略:

2.2.1 TrimStrategy.MAXLEN

java 复制代码
StreamAddArgs.entries(map).trim(TrimStrategy.MAXLEN, 1000) 基于消息数量进行修剪,如果流长度超过1000条,则修剪流,保留最多1000条最新的消息

2.2.2 TrimStrategy.MINID

java 复制代码
StreamAddArgs.entries(map).trim(TrimStrategy.MINID, 1000) 基于消息ID进行修剪,添加消息时,修剪掉所有ID小于1000的消息
特性 MAXLEN策略 MINID策略
修剪依据 消息数量 消息ID
保留内容 最新的N条消息 ID大于等于阈值的所有消息
适用场景 限制流的大小 基于时间/ID范围清理消息
参数意义 要保留的消息数量 要保留的最小消息ID
动态性 总是保留最新的N条 保留的ID范围固定

3. 添加消息的API

3.1 add

StreamMessageId add(StreamAddArgs<K, V> args) 向 Redis Stream 中添加一条消息,消息内容由 StreamAddArgs 对象定义
java 复制代码
// 向流中添加消息,并制定了消息的修剪策略
StreamMessageId streamMessageId1 = redissonStream.add(StreamAddArgs.entries(orderData).trim(TrimStrategy.MAXLEN, 1000));

3.2 addAll

StreamMessageId addAll(Map<K, V> entries, int trimLen, boolean trimStrict) 向 Redis Stream 中添加消息,并制定消息行为

StreamMessageId addAll(Map<K, V> entries, int trimLen, boolean trimStrict);

  • param1: Map<K, V> entries 每个键值对代表一条消息的内容
  • param2: int trimLen 最大长度限制(Max Length), 如果消息数量超过这个限制,Redis 会自动删除最旧的消息。默认为 0,表示不限制。
  • param3: boolean trimStrict 表示是否启用近似修剪(Approximate Trimming)。如果为 true,Redis 会以近似的方式修剪消息,可能会保留比指定的最大长度略多的消息。 如果为 false,Redis 会精确地修剪消息,确保消息数量严格不超过指定的最大长度
java 复制代码
    
StreamMessageId streamMessageId2 = redissonStream.addAll(orderData, 1000, true);

4. StreamMessageId

StreamMessageId 是 Redisson 中用于标识 Redis Stream 消息的唯一标识符类。它对应于 Redis 原生 Stream 的 ID 结构,并提供了丰富的操作方法。

4.1 基本结构

Redis Stream 的消息 ID 格式为:<millisecondsTime>-<sequenceNumber>,例如 1631234567890-0,也就是时间戳-序列号,Redis Stream 的消息 ID 是单调递增的,后添加的消息 ID 一定大于先添加的

在 Redisson 中,StreamMessageId 封装了这种结构:

java 复制代码
public class StreamMessageId implements Comparable<StreamMessageId> {
    private final long timestamp;
    private final long sequenceNumber;
    // 其他方法和常量...
}

4.2 特殊常量

Redisson 提供了几个特殊的 StreamMessageId 常量:

常量 说明
MIN 表示最小可能的 ID (0-0)
MAX 表示最大可能的 ID (Long.MAX_VALUE-Long.MAX_VALUE)
NEWEST 表示只读取最新消息
ALL 表示所有消息
NEVER_DELIVERED 表示从未投递的消息
AUTO_GENERATED 表示由 Redis 自动生成 ID

5. 读取消息API

|----------------------------------------------------------------------|----------------------|
| read(int count, StreamMessageId... ids) | 读取流中的消息,最多返回count条消息 |
| read(int count, long timeout, TimeUnit unit, StreamMessageId... ids) | 在指定时间内读取流中的count条消息 |

java 复制代码
// 读取流中的最新消息,最多返回10条消息
Map<StreamMessageId, Map<String, Object>> read1 = redissonStream.read(10, StreamMessageId.NEWEST);

// 在5000毫秒内读取流中的最新消息,最多返回10条消息
// 如果在指定时间内没有新消息,将返回空
Map<StreamMessageId, Map<String, Object>> read2 = redissonStream.read(10, 5000, TimeUnit.MILLISECONDS, StreamMessageId.NEWEST);

6.消费者组操作

6.1 创建消费者组

java 复制代码
/ 从最新消息开始消费,若存在则会抛出异常
stream.createGroup("myGroup");

6.2 StreamReadGroupArgs

StreamReadGroupArgs 是 Redisson 中用于配置消费者组读取操作的参数构建器,它允许精细控制从消费者组读取消息的行为。

6.2.1 初始方法

|---------------------------------|----------------------------------|
| neverDelivered() | 只读取从未投递给任何消费者的消息 |
| greaterThan(StreamMessageId id) | 从指定的消息ID之后开始读取消息(即只读取比给定ID更大的消息) |

6.2.2 可选配置方法
方法 描述 默认值
count(int count) 设置每次读取的最大消息数 1
timeout(long timeout, TimeUnit unit) 设置阻塞读取的超时时间 无(非阻塞)
noAck() 设置读取后不自动确认消息 需要确认

7. 消费者组读取

|----------------------------------------------------------------------------|-------------|
| readGroup(String groupName, String consumerName, StreamReadGroupArgs args) | 读取消息并制定读取策略 |

java 复制代码
redissonStream.readGroup("myGroup", "Consumer1", StreamReadGroupArgs.neverDelivered());

8.确认消息

java 复制代码
// 确认一条消息已处理
stream.ack("myGroup", messageId);

// 确认多条消息已处理
stream.ack("myGroup", messageId1, messageId2, messageId3);

9. 删除消息API

9.1 StreamTrimArgs

StreamTrimArgs 是 Redisson 中用于控制 Redis Stream 修剪行为的参数类,它允许您精确控制如何修剪(trim)Stream 中的消息。

9.1.1 初始方法

|----------------------------------|-----------------------------|
| maxLen(int threshold) | 设置 Stream 的最大长度(即最多保留的消息数量) |
| minId(StreamMessageId messageId) | 指定基于最小消息ID的修剪策略 |

9.1.2 可选配置方法

|-----------------|-------------------|
| limit(int size) | 设置每次修剪操作最多删除的消息数量 |
| noLimit() | 无删除限制 |

9.2 初始方法

|--------------------------------------------|----------------------------------|
| remove(StreamMessageId... ids) | 删除单个或多个消息 |
| trim(StreamTrimArgs args) | 通过 StreamTrimArgs 参数对象精细控制修剪行为 |
| trim(int threshold) | 默认保留最新的 threshold 条消息 |
| trim(TrimStrategy strategy, int threshold) | 指定修剪策略的修剪方法 |

9.2.1 remove与trim主要区别对比
特性 remove trim
删除方式 精确删除指定消息 按规则批量删除消息
删除目标 任意位置的消息 通常是最旧的消息
控制粒度 细粒度,精确到每条消息 粗粒度,按数量或策略
使用场景 删除特定错误或不需要的消息 维护流的大小,防止无限增长
性能影响 对单个消息操作,影响较小 可能影响大量消息,操作较重
返回值 返回删除的消息数量 返回删除的消息数量
9.2.2 trim 方法执行时机问题解析

Redisson 的 trim 方法确实需要在添加消息(add之前执行,否则可能会报错。这是由 Redis Stream 的特性和 Redisson 的实现方式共同决定的

对于trim(StreamTrimArgs args)必须要在add方法之前进行设置,而对于其他的trim方法,可以在add前后执行删除操作。

java 复制代码
@Test
    public void trimTest2(){
        for (int i = 0; i < 10; i++) {
            orderData.put("orderId", "order" + i);
        }
        redissonStream.add(StreamAddArgs.entries(orderData));
        int length = redissonStream.getInfo().getLength();
        System.out.println("消息队列长度为:"+length); // 2
        redissonStream.trim(TrimStrategy.MAXLEN,1);
        length = redissonStream.getInfo().getLength();
        System.out.println("消息队列长度为:"+length);  // 1

    }

10. 获取流信息

|-----------|--------------------|
| getInfo() | 获取流的元信息(如长度、消费者组等) |

11.范围查询

|------------------------------------------------------------------|------------------------------------|
| getPendingInfo(String groupName) | 获取 Redis Stream 中指定消费者组的未确认消息的相关信息 |
| range(int count, StreamMessageId startId, StreamMessageId endId) | 获取指定范围内的count条消息 |
| range(StreamMessageId startId, StreamMessageId endId) | 获取指定范围内的所有消息 |

11.1 getPendingInfo

这里我们需要注意一下:

  1. 消费者组的未确认消息:方法返回的是某个消费者组中已经被分配给消费者但尚未被确认处理的消息
  2. 在调用remove方法删除流中的消息,若删除的消息并未被消费者ack,那么消费者组中的penging消息经不会被删除!
11.1.1 可选配置方法

|----------------|--------------|
| getLowestId() | 获取最小Id的消息ID |
| getHighestId() | 获取最大Id的消息ID |
| getTotal() | 获取未确认消息的消息数量 |

11.1.2 getPendingInfo方法演示案例

下面将读取消息队列中最早的消费者组的未确认消息ID

java 复制代码
@Test
    public void PendingInfoTest() {
        redissonStream.createGroup("myGroup");
        Map<String, Object> orderData = new HashMap<>();
        orderData.put("orderId", "123456");
        orderData.put("userId", "123456");
        orderData.put("status", "未处理");
        redissonStream.add(StreamAddArgs.entries(orderData));
        Map<StreamMessageId, Map<String, Object>> readMessages = redissonStream.readGroup(
                "myGroup", "worker1", StreamReadGroupArgs.neverDelivered().count(1)
        );
        if (readMessages.isEmpty()) {
            System.out.println("没有消息被读取!");
        } else {
            System.out.println("已读取消息:" + readMessages);
        }
        PendingResult myGroup = redissonStream.getPendingInfo("myGroup");
        StreamMessageId lowestId = myGroup.getLowestId();
        System.out.println(lowestId);
    }

11.2 range方法演示案例

java 复制代码
@Test
    public void rangeTest() {
        redissonStream.createGroup("myGroup");
        Map<String, Object> orderData = new HashMap<>();
        orderData.put("orderId", "123456");
        orderData.put("userId", "123456");
        orderData.put("status", "未处理");
        redissonStream.add(StreamAddArgs.entries(orderData));

        Map<StreamMessageId, Map<String, Object>> range = redissonStream.range(10, StreamMessageId.MIN, StreamMessageId.MAX);
        if (range.isEmpty()){
            System.out.println("读取失败");
        }
    }

四、完整使用案例:订单处理系统

下面我们将模拟一个最简单的消息队列模式

包含:

  1. 订单创建服务(生产者)

  2. 订单处理服务(消费者组)

使用技术:Spring+Maven+Redis 5以上版本

1.依赖引入和Redis连接配置

pom.xml

XML 复制代码
<!-- Redisson 依赖 -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.16.2</version>
        </dependency>

        <!-- Spring Context 依赖 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.24</version> <!-- 请根据需要选择最新版本 -->
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>

application.yaml

html 复制代码
server:
  port: 8081
spring:
  redis:
    redisson:
      config:
        address: "redis://127.0.0.1:6379"
        password:
        database: 1

2. 初始化 Redisson 配置

java 复制代码
@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        return Redisson.create(config);
    }

}

3. 订单生产者服务

java 复制代码
package com.hl.redisMQ;

import org.redisson.api.RStream;
import org.redisson.api.RedissonClient;
import org.redisson.api.StreamMessageId;
import org.redisson.api.stream.StreamAddArgs;
import org.redisson.client.RedisException;

import java.util.HashMap;
import java.util.Map;
import java.util.Random;

/*
* 模拟订单生产者
* */
public class OrderProducer {
    private final RStream<String, String> orderStream;

    public OrderProducer(RedissonClient redisson) {
        // 获取Stream消息流,若不存在则创建
        this.orderStream = redisson.getStream("orders");
        try {
            this.orderStream.createGroup("processing_group");
        } catch (RedisException e) {
            System.out.println("消息组已存在,无需创建");
        }

    }

    public String createOrder(String userId) {
        String orderId = "order_" + new Random().nextInt(10000);
        Map<String, String> orderData = new HashMap<>();
        orderData.put("orderId", orderId);
        orderData.put("userId", userId);
        orderData.put("orderName", "极品奥特曼玩具!!");
        orderData.put("status", "未处理");
        orderData.put("timestamp", String.valueOf(System.currentTimeMillis()));

        // 添加消息到Stream
        StreamMessageId streamMessageId = orderStream.add(StreamAddArgs.entries(orderData));

        System.out.println("订单创建成功: " + orderId + ", 消息ID: " + streamMessageId);
        return orderId;
    }
}

4. 订单处理消费者服务

java 复制代码
package com.hl.redisMQ;

import org.redisson.api.RStream;
import org.redisson.api.RedissonClient;
import org.redisson.api.StreamMessageId;
import org.redisson.api.stream.StreamReadGroupArgs;

import java.util.Map;
// 处理消费者服务
public class OrderProcessingConsumer {
    private final RStream<String, String> orderStream;
    private final String consumerName;

    public OrderProcessingConsumer(RedissonClient redisson, String consumerName) {
        this.orderStream = redisson.getStream("orders");
        this.consumerName = consumerName;
    }

    /**
     * 启动处理订单消息的线程
     * 该方法创建并启动一个新的线程,用于不断尝试读取和处理订单消息
     * 线程会一直运行,直到被显式中断
     */
    public void startProcessing() {
        new Thread(() -> {
            // 检测线程是否被中断
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    // 从消费者组读取未处理的消息
                    Map<StreamMessageId, Map<String, String>> messages =
                            orderStream.readGroup("processing_group", consumerName,
                                    /*创建一个读取从未被交付的消息的参数对象*/
                                    StreamReadGroupArgs.neverDelivered().count(1));

                    if (messages.isEmpty()) {
                        // 如果没有消息,休眠1秒后继续尝试读取
                        Thread.sleep(1000);
                        continue;
                    }

                    // 遍历读取到的消息并进行处理
                    messages.forEach((id, orderData) -> {
                        try {
                            // 打印处理订单的开始信息
                            System.out.println(consumerName + " 开始处理订单: " + orderData.get("orderId"));
                            // 模拟业务处理,这里简单地让线程休眠一段时间
                            Thread.sleep(1500);

                            // 更新订单状态为已处理,并添加到消息流中
                            orderData.put("status", "已处理");

                            // 确认消息已处理,通过acknowledge机制
                            orderStream.ack("processing_group", id);
                            // 打印处理订单的完成信息
                            System.out.println(consumerName + " 处理完成: " + orderData.get("orderId"));
                        } catch (Exception e) {
                            // 打印异常信息
                            e.printStackTrace();
                        }
                    });
                } catch (InterruptedException e) {
                    // 如果线程被中断,设置当前线程的中断状态
                    Thread.currentThread().interrupt();
                } catch (Exception e) {
                    // 打印其他异常信息,并休眠5秒后继续尝试读取消息
                    e.printStackTrace();
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException ex) {
                        // 如果休眠期间线程被中断,设置当前线程的中断状态
                        Thread.currentThread().interrupt();
                    }
                }
            }
        }).start();
    }

}

5. 主程序测试

java 复制代码
import com.hl.redisMQ.OrderProcessingConsumer;
import com.hl.redisMQ.OrderProducer;
import org.redisson.api.RedissonClient;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class RedissonMQTest1 {
    public static void main(String[] args) throws InterruptedException {
        // 手动加载Spring上下文
        ApplicationContext context = new AnnotationConfigApplicationContext("com.hl");
        RedissonClient redissonClient = context.getBean(RedissonClient.class);

        // 创建生产者
        OrderProducer producer = new OrderProducer(redissonClient);

        // 创建处理消费者(启动2个处理实例)
        OrderProcessingConsumer processor1 = new OrderProcessingConsumer(redissonClient, "processor_1");
        OrderProcessingConsumer processor2 = new OrderProcessingConsumer(redissonClient, "processor_2");
        processor1.startProcessing();
        processor2.startProcessing();


        // 模拟生成订单
        for (int i = 1; i <= 10; i++) {
            producer.createOrder("user_" + i);
            Thread.sleep(300);
        }

        // 保持运行
        Thread.sleep(30000);

        // 关闭
//        redissonClient.shutdown();

    }
}

6.运行结果

消息发送给消费者组后,绑定的消费者就开始竞争处理消息了,处理后的消息标记为ack,表示消息已经处理过了

7.关键点解析

  1. 消息ACK机制 :处理完成后必须调用ack()确认,否则消息会重新投递

  2. 消费者负载均衡:同一消费者组的多个消费者会自动分配消息

  3. 消息持久化:所有消息会保留在Redis中,直到被所有消费者组消费

  4. 消息格式:使用Map结构存储消息内容,方便扩展字段

五、Redis Stream 定向消息路由方案

在 Redis Stream 中,默认情况下消息会被所有消费者组接收(每个消费者组都能读取完整的消息流),但可以通过以下方案实现消息的定向路由,将特定消息只发送给指定的消费者组:

1.实现方案比较

方案 原理 优点 缺点
多Stream分离 为每个消费者组创建独立的Stream 完全隔离,性能最佳 需要生产者维护多个Stream
消息标签+过滤消费 在消息中添加路由标签,消费者自行过滤 灵活性高 消费者需要处理不相关消息
中间路由服务 增加路由层负责消息分发 路由逻辑灵活 增加系统复杂度
消费者组动态订阅 动态修改消费者组的读取位置 实时性高 实现复杂

2.多Stream分离 + 路由Key

1. 架构设计

生产者 → 主Stream → 路由服务 → 特定消费者组的Stream → 消费者

六、完整使用案例:订单处理反馈系统

包含:

  1. 订单创建服务(生产者)

  2. 订单处理服务(消费者组)

  3. 订单通知服务(消费者组)

  4. 路由服务

1.路由服务实现

在该类中将完成一切消息队列的初始化

java 复制代码
package com.hl.redisMQ2;

import org.redisson.api.RStream;
import org.redisson.api.RedissonClient;
import org.redisson.api.StreamMessageId;
import org.redisson.api.stream.StreamAddArgs;
import org.redisson.api.stream.StreamReadGroupArgs;
import org.redisson.client.RedisException;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/*
* 路由服务实现
* */
public class StreamRouterService {
    private final RedissonClient redissonClient;
    // 主Stream
    private final RStream<String, String> mainStream;
    // 消费者组对应的Stream
    private final Map<String, RStream<String, String>> targetStreams;
    public final String RedissonRouterName = "RedissonRouter";

    public StreamRouterService(RedissonClient redisson) {
        this.redissonClient = redisson;
        // 1.获取主Stream并初始化
        this.mainStream = redissonClient.getStream("mainStream");
        try {
            mainStream.createGroup("main_group");
        } catch (RedisException e) {
            // 组已存在
            System.out.println("mainStream的消费者组main_group已存在,无需创建");
        }

        // 2.初始化消费者组对应的Stream并初始化
        this.targetStreams = new ConcurrentHashMap<>();
        // 2.1订单处理服务对应的Stream
        RStream<String, String> processingStream = redissonClient.getStream("processingStream");
        try {
            processingStream.createGroup("processor_group");
        } catch (RedisException e) {
            // 组已存在
            System.out.println("processingStream的消费者组processor_group已存在,无需创建");
        }
        targetStreams.put("processingStream", processingStream);

        // 2.2订单通知消费者对应的Stream
        RStream<String, String> notifyStream = redissonClient.getStream("notifyStream");
        try {
            notifyStream.createGroup("notify_group");
        } catch (RedisException e) {
            // 组已存在
            System.out.println("notifyStream的消费者组notify_group已存在,无需创建");
        }
        targetStreams.put("notifyStream", notifyStream);

        // 启动路由任务
        startRouting();
    }

    private void startRouting() {
        new Thread(() -> {

            while (!Thread.currentThread().isInterrupted()) {
                try {
                    // 从主Stream中读取从未被消费过的消息
                    Map<StreamMessageId, Map<String, String>> messages = mainStream.readGroup(
                            "main_group", "main_worker1" ,StreamReadGroupArgs.neverDelivered().count(10)
                    );

                    if (messages.isEmpty()) {
                        Thread.sleep(1000);
                        continue;
                    }

                    // 处理每条消息
                    for (Map.Entry<StreamMessageId, Map<String, String>> entry : messages.entrySet()) {
                        Map<String, String> msg = entry.getValue();

                        // 根据消息类型路由
                        String msgType = msg.get("type");
                        switch (msgType) {
                            case "PROCESSING":
                                forwardToGroup(msg, "processingStream");
                                break;
                            case "NOTIFY":
                                forwardToGroup(msg, "notifyStream");
                                break;
                            default:
                                // 默认路由到所有组
                                forwardToAll(msg);
                        }
                        mainStream.ack("main_group", entry.getKey());
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException ex) {
                        Thread.currentThread().interrupt();
                    }
                }
            }
        }).start();
    }

    private void forwardToGroup(Map<String, String> msg, String group) {
        RStream<String, String> target = targetStreams.get(group);
        if (target != null) {
            target.add(StreamAddArgs.entries(msg));
            System.out.println("路由消息到组: " + group + ", 内容: " + msg);
        }
    }

    private void forwardToAll(Map<String, String> msg) {
        targetStreams.values().forEach(stream -> {
            stream.add(StreamAddArgs.entries(msg));
        });
        System.out.println("广播消息到所有组, 内容: " + msg);
    }
}

2.订单生产者服务

java 复制代码
package com.hl.redisMQ2;

import org.redisson.api.RStream;
import org.redisson.api.RedissonClient;
import org.redisson.api.StreamMessageId;
import org.redisson.api.stream.StreamAddArgs;

import java.util.HashMap;
import java.util.Map;
import java.util.Random;

public class OrderProducer {
    private final RStream<String, String> mainStream;

    public OrderProducer(RedissonClient redisson) {
        this.mainStream = redisson.getStream("mainStream");
    }

    public void createPaymentOrder(String userId) {
        Map<String, String> data = new HashMap<>();
        data.put("userId", userId);
        data.put("orderId", new Random().nextInt(10000)+"");
        data.put("type", "PROCESSING");
        StreamMessageId streamMessageId = mainStream.add(StreamAddArgs.entries(data));
        System.out.println("订单创建成功: " + data.get("orderId") + ", 消息ID: " + streamMessageId);
    }

}

3.订单处理服务

从路由分配的消息流processingStream中读取消息,并进行处理,处理完成后将消息再添加到主消息流mainStream中

java 复制代码
package com.hl.redisMQ2;

import org.redisson.api.RStream;
import org.redisson.api.RedissonClient;
import org.redisson.api.StreamMessageId;
import org.redisson.api.stream.StreamAddArgs;
import org.redisson.api.stream.StreamReadGroupArgs;
import org.redisson.client.RedisException;

import java.util.Map;
/*
* 订单处理服务
*   从路由分配的消息流processingStream中读取消息,并进行处理,处理完成后将消息再添加到主消息流mainStream中
* */
public class OrderProcessingConsumer {
    private final RStream<String, String> mainStream;
    private final RStream<String, String> processingStream;

    public OrderProcessingConsumer(RedissonClient redisson) {
        this.processingStream = redisson.getStream("processingStream");
        this.mainStream = redisson.getStream("mainStream");
    }

    public void startConsuming() {
        new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    Map<StreamMessageId, Map<String, String>> messages =
                            processingStream.readGroup("processor_group", "processor_worker1",
                                    StreamReadGroupArgs.neverDelivered().count(1));

                    if (messages.isEmpty()) {
                        Thread.sleep(1000);
                        continue;
                    }

                    messages.forEach((id, msg) -> {
                        try {
                            System.out.println("处理支付订单: " + msg.get("orderId"));
                            // 模拟业务处理,这里简单地让线程休眠一段时间
                            Thread.sleep(1500);
                            processingStream.ack("processor_group", id);
                            // 修改消息类型,重新放到主消息流中
                            msg.put("type", "NOTIFY");
                            mainStream.add(StreamAddArgs.entries(msg));
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    });
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

4.订单通知服务

java 复制代码
package com.hl.redisMQ2;

import org.redisson.api.RStream;
import org.redisson.api.RedissonClient;
import org.redisson.api.StreamMessageId;
import org.redisson.api.stream.StreamReadGroupArgs;

import java.util.Map;

/*
 * 订单通知消费者服务
 * */
public class OrderNotificationConsumer {
    private final RStream<String, String> notifyStream;

    public OrderNotificationConsumer(RedissonClient redisson) {
        this.notifyStream = redisson.getStream("notifyStream");
    }

    public void startNotification() {
        new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    // 从通知消费者组读取消息
                    Map<StreamMessageId, Map<String, String>> messages =
                            notifyStream.readGroup("notify_group", "notify_worker1",
                                    StreamReadGroupArgs.neverDelivered().count(1));

                    if (messages.isEmpty()) {
                        Thread.sleep(1000);
                        continue;
                    }

                    messages.forEach((id, msg) -> {
                        try {

                            System.out.println("发送订单处理完成通知给用户: " + msg.get("userId"));
                            // 模拟发送通知
                            Thread.sleep(500);

                            // 确认消息已处理
                            notifyStream.ack("notify_group", id);

                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    });
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

5.主程序测试

java 复制代码
import com.hl.redisMQ2.OrderNotificationConsumer;
import com.hl.redisMQ2.OrderProcessingConsumer;
import com.hl.redisMQ2.OrderProducer;
import com.hl.redisMQ2.StreamRouterService;
import org.redisson.api.RedissonClient;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class RedissonMQTest2 {
    public static void main(String[] args) throws InterruptedException {
        ApplicationContext context = new AnnotationConfigApplicationContext("com.hl");
        RedissonClient redissonClient = context.getBean(RedissonClient.class);

        // 先激活路由服务
        StreamRouterService routerService = new StreamRouterService(redissonClient);
        System.out.println(routerService.RedissonRouterName+"已启动");
        // 创建生产者
        OrderProducer producer = new OrderProducer(redissonClient);
        // 创建处理消费者
        OrderProcessingConsumer processorConsumer = new OrderProcessingConsumer(redissonClient);
        processorConsumer.startConsuming();
        // 创建通知消费者
        OrderNotificationConsumer notificationConsumer = new OrderNotificationConsumer(redissonClient);
        notificationConsumer.startNotification();

        // 模拟生成订单
        for (int i = 1; i <= 10; i++) {
            producer.createPaymentOrder("user_" + i);
            Thread.sleep(300);
        }
        // 保持运行
        Thread.sleep(30000);
    }
}

6.运行结果

6.1 订单处理服务生效

6.2 订单通知服务生效

7.关键点解析

  1. 完全隔离:每个消费者组有独立的Stream,互不干扰

  2. 性能高效:消费者只需处理自己关心的消息

  3. 灵活路由:可以根据任意消息字段进行路由决策

  4. 扩展性强:新增消费者组无需修改现有消费者代码

  5. 维护简单:每个Stream可以独立监控和管理

七、Redis Stream 消息清理策略

在 Redis Stream 中,消息被 ACK 后不会自动删除,而是会保留在 Stream 中。如果不执行一些消息的清理策略,那么会使内存无限增长。

1. 消费者延迟ACK消息清理策略

该策略思路是:消费者消费消息后不立即ack消息,而是在执行清理策略时ack消息后再删除

1.1 生产者服务

java 复制代码
package com.hl.mqclear1;

import org.redisson.api.RStream;
import org.redisson.api.RedissonClient;
import org.redisson.api.StreamMessageId;
import org.redisson.api.stream.StreamAddArgs;
import org.redisson.api.stream.TrimStrategy;
import org.redisson.client.RedisException;

import java.util.HashMap;
import java.util.Map;
import java.util.Random;

/*
 * 模拟订单生产者
 * */
public class Producer {
    private final RStream<String, String> redissonStream;

    public Producer(RedissonClient redisson) {
        // 获取Stream消息流,若不存在则创建
        this.redissonStream = redisson.getStream("redissonStream");
        try {
            this.redissonStream.createGroup("processing_group");
        } catch (RedisException e) {
            System.out.println("消息组已存在,无需创建");
        }

    }

    public String createMSG(String userId) {
        String orderId = "order_" + new Random().nextInt(10000);
        Map<String, String> orderData = new HashMap<>();
        orderData.put("orderId", orderId);
        orderData.put("userId", userId);
        orderData.put("orderName", "极品奥特曼玩具!!");

        // 添加消息到Stream,并制定修剪策略:基于条目数量,最多保存1000条数据
        StreamAddArgs<String, String> arg = StreamAddArgs.entries(orderData).trim(TrimStrategy.MAXLEN, 1000);
        StreamMessageId streamMessageId = redissonStream.add(arg);

        System.out.println("订单创建成功: " + orderId + ", 消息ID: " + streamMessageId);
        return orderId;
    }
}

1.2 消费者服务

java 复制代码
package com.hl.mqclear1;

import org.redisson.api.PendingResult;
import org.redisson.api.RStream;
import org.redisson.api.RedissonClient;
import org.redisson.api.StreamMessageId;
import org.redisson.api.stream.StreamReadGroupArgs;

import java.util.*;

// 处理消费者服务
public class Consumer {
    private final RStream<String, String> redissonStream;
    private final String consumerName;

    public Consumer(RedissonClient redisson, String consumerName) {
        this.redissonStream = redisson.getStream("redissonStream");
        this.consumerName = consumerName;
    }

    /**
     * 启动处理订单消息的线程
     * 该方法创建并启动一个新的线程,用于不断尝试读取和处理订单消息
     * 线程会一直运行,直到被显式中断
     */
    public void startProcessing() {
        new Thread(() -> {
            // 检测线程是否被中断
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    // 从消费者组读取未处理的消息
                    Map<StreamMessageId, Map<String, String>> messages =
                            redissonStream.readGroup("processing_group", consumerName,
                                    /*创建一个读取从未被交付的消息的参数对象*/
                                    StreamReadGroupArgs.neverDelivered().count(1));

                    if (messages.isEmpty()) {
                        // 如果没有消息,休眠1秒后继续尝试读取
                        // 没有消息时执行清理
                        cleanUpProcessedMessages();
                        Thread.sleep(5000);
                        continue;
                    }

                    // 遍历读取到的消息并进行处理
                    messages.forEach((id, orderData) -> {
                        try {
                            // 打印处理订单的开始信息
                            System.out.println(consumerName + " 开始处理订单: " + orderData.get("orderId"));
                            // 模拟业务处理,这里简单地让线程休眠一段时间
                            Thread.sleep(1500);

                            // 确认消息已处理,通过acknowledge机制
                            // 打印处理订单的完成信息
                            System.out.println(consumerName + " 处理完成: " + orderData.get("orderId"));
                        } catch (Exception e) {
                            // 打印异常信息
                            e.printStackTrace();
                        }
                    });
                } catch (InterruptedException e) {
                    // 如果线程被中断,设置当前线程的中断状态
                    Thread.currentThread().interrupt();
                } catch (Exception e) {
                    // 打印其他异常信息,并休眠5秒后继续尝试读取消息
                    e.printStackTrace();
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException ex) {
                        // 如果休眠期间线程被中断,设置当前线程的中断状态
                        Thread.currentThread().interrupt();
                    }
                }
            }
        }).start();
    }

    private void cleanUpProcessedMessages() {
        System.out.println("开始清理处理完成的消息");
        try {
            // 获取消费者组最早未ack的消息
            PendingResult pending = redissonStream.getPendingInfo("processing_group");
            // 如果没有消息,直接返回
            if (pending.getTotal() == 0) {
                System.out.println("没有待清理的消息");
                return;
            }

            // 等待2秒,确保这部分消息已处理
            Thread.sleep(2000);

            // 获取所有 pending 的消息 ID
            Map<StreamMessageId, Map<String, String>> processingGroup = redissonStream.readGroup("processing_group", consumerName,
                    StreamReadGroupArgs.greaterThan(pending.getLowestId()).count((int) pending.getTotal() - 1));
            List<StreamMessageId> ids = new ArrayList<>();
            ids.addAll(processingGroup.keySet());
            ids.add(pending.getLowestId());

            // 确认这些消息已处理
            redissonStream.ack("processing_group", ids.toArray(new StreamMessageId[0]));
            System.out.println("确认 " + ids.size() + " 条消息已处理");

            // 删除这些消息
            long remove = redissonStream.remove(ids.toArray(new StreamMessageId[0]));
            System.out.println("清理处理完成的消息成功: " + remove);
        } catch (InterruptedException e) {
            System.out.println("清理消息过程中线程被中断");
            Thread.currentThread().interrupt();
        } catch (Exception e) {
            System.out.println("清理消息过程中发生异常");
            e.printStackTrace();
        }
    }



}

1.3 主程序测试

java 复制代码
import com.hl.mqclear1.Consumer;
import com.hl.mqclear1.Producer;
import org.redisson.api.RedissonClient;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class MQClear1 {
    public static void main(String[] args) throws InterruptedException {
        ApplicationContext context = new AnnotationConfigApplicationContext("com.hl");
        RedissonClient redissonClient = context.getBean(RedissonClient.class);

        Producer producer = new Producer(redissonClient);
        for (int i = 0; i < 10; i++) {
            producer.createMSG("user_"+i);
        }

        Consumer consumer = new Consumer(redissonClient, "consumer_1");
        consumer.startProcessing();
        Thread.sleep(30000);
    }
}

1.4 运行结果

2. 消费者即时ACK+立即删除策略

这种的消费策略简单粗暴,相比于第一种实现更加简单

2.1 消费者服务

java 复制代码
package com.hl.mqclear2;

import org.redisson.api.*;
import org.redisson.api.stream.StreamReadGroupArgs;

import java.util.*;
import java.util.stream.Collectors;

// 处理消费者服务
public class Consumer {
    private final RStream<String, String> redissonStream;
    private final String consumerName;

    public Consumer(RedissonClient redisson, String consumerName) {
        this.redissonStream = redisson.getStream("redissonStream");
        this.consumerName = consumerName;
    }

    /**
     * 启动处理订单消息的线程
     * 该方法创建并启动一个新的线程,用于不断尝试读取和处理订单消息
     * 线程会一直运行,直到被显式中断
     */
    public void startProcessing() {
        new Thread(() -> {
            // 检测线程是否被中断
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    // 从消费者组读取未处理的消息
                    Map<StreamMessageId, Map<String, String>> messages =
                            redissonStream.readGroup("processing_group", consumerName,
                                    /*创建一个读取从未被交付的消息的参数对象*/
                                    StreamReadGroupArgs.neverDelivered().count(1));

                    if (messages.isEmpty()) {
                        // 如果没有消息,休眠1秒后继续尝试读取
                        // 没有消息时执行清理
                        Thread.sleep(5000);
                        continue;
                    }

                    // 遍历读取到的消息并进行处理
                    messages.forEach((id, orderData) -> {
                        try {
                            // 打印处理订单的开始信息
                            System.out.println(consumerName + " 开始处理订单: " + orderData.get("orderId"));
                            // 模拟业务处理,这里简单地让线程休眠一段时间
//                            Thread.sleep(1500);

                            // 确认消息已处理,通过acknowledge机制
                            redissonStream.ack("processing_group", id);
                            redissonStream.remove(id);
                            // 打印处理订单的完成信息
                            System.out.println(consumerName + " 处理完成: " + orderData.get("orderId"));
                        } catch (Exception e) {
                            // 打印异常信息
                            e.printStackTrace();
                        }
                    });
                } catch (InterruptedException e) {
                    // 如果线程被中断,设置当前线程的中断状态
                    Thread.currentThread().interrupt();
                } catch (Exception e) {
                    // 打印其他异常信息,并休眠5秒后继续尝试读取消息
                    e.printStackTrace();
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException ex) {
                        // 如果休眠期间线程被中断,设置当前线程的中断状态
                        Thread.currentThread().interrupt();
                    }
                }
            }
        }).start();
    }

}

2.2 运行结果

3. 两种消息清理策略详细对比

3.1. 核心机制对比

对比维度 方案一:延迟ACK+批量清理 方案二:立即ACK+立即删除
ACK时机 在清理策略执行时批量ACK 消息处理完成后立即ACK
删除时机 ACK后批量删除 ACK后立即删除
状态记录 依赖Redis原生的pending列表 不需要额外记录状态
实现复杂度 较高(需维护清理逻辑) 较低(线性处理)

3.2. 性能表现对比

性能指标 方案一 方案二
吞吐量 高(批量操作减少IO次数) 适合高并发场景(每秒千级以上) 较低(每条消息独立操作) 适合中低吞吐场景(每秒数百条)
延迟 较高(消息存在延迟清理) 低(即时处理)
Redis负载 负载波动大(批量操作时短时负载高) 负载平稳但持续
网络开销 少(批量操作减少网络往返) 多(每条消息2次操作:ACK+删除)

3.3. 可靠性对比

可靠性方面 方案一 方案二
消息丢失风险 低(依赖Redis原生机制) 中(ACK后删除前若崩溃,消息既不在pending也不在stream中)
重复消费风险 高(未及时ACK的消息可能被重新投递) 低(ACK后立即移除可见性)
故障恢复 复杂(需处理pending状态) 简单(只需重试未ACK消息)
数据一致性 最终一致 即时一致

3.4. 资源占用对比

资源类型 方案一 方案二
内存占用 高(已处理消息会保留到清理时) 示例:若每秒1000条消息,清理间隔5秒,可能积压5000条消息 低(立即释放)
CPU消耗 集中消耗(清理时) 均匀消耗
存储压力 较高(消息保留时间长) 最低

3.5. 典型应用场景

方案一更适合

java 复制代码
// 电商大促场景
- 高峰时段订单量激增(每秒万级)
- 允许短暂重复处理(订单创建幂等)
- 有规律的低谷期可执行清理
- 示例:双11订单处理系统

方案二更适合

java 复制代码
// 金融交易场景
- 消息量适中(每秒数百条)
- 要求严格不重复处理
- 内存资源受限
- 示例:实时支付清算系统

对于Stream的消息修剪策略,网上可能会有更好的策略,这里的两种只是我能想出的最简单的,希望有更懂得的朋友可以分享交流。

相关推荐
我命由我123451 小时前
Spring Boot 自定义日志打印(日志级别、logback-spring.xml 文件、自定义日志打印解读)
java·开发语言·jvm·spring boot·spring·java-ee·logback
极客天成ScaleFlash5 小时前
极客天成NVFile:无缓存直击存储性能天花板,重新定义AI时代并行存储新范式
人工智能·缓存
杉之6 小时前
常见前端GET请求以及对应的Spring后端接收接口写法
java·前端·后端·spring·vue
morris1316 小时前
【redis】redis实现分布式锁
数据库·redis·缓存·分布式锁
爱的叹息8 小时前
spring boot集成reids的 RedisTemplate 序列化器详细对比(官方及非官方)
redis
canonical_entropy8 小时前
Nop入门-如何通过配置扩展服务函数的返回对象
spring·mvc·graphql
小李同学_LHY9 小时前
三.微服务架构中的精妙设计:服务注册/服务发现-Eureka
java·spring boot·spring·springcloud
weitinting9 小时前
Ali linux 通过yum安装redis
linux·redis
非ban必选9 小时前
spring-ai-alibaba第四章阿里dashscope集成百度翻译tool
java·人工智能·spring
非ban必选9 小时前
spring-ai-alibaba第五章阿里dashscope集成mcp远程天气查询tools
java·后端·spring