自定义注解实现 Redis Stream 消息监听

前提

在研究websocket集群解决方案的时候发现

可以通过消息中间件的广播模式或者redis的发布订阅 (Pub/Sub)模式来通知其他服务器的websocket节点

那 redis stream 也能实现消息的发布和订阅,不仅比 (Pub/Sub)更可靠,而且比其他消息中间件延迟更低

在实时聊天室、游戏状态同步、轻量级任务队列用 redis stream 作为中间件性能也较为不错

kafka 有注解(@KafkaListener)就可以实现kafka的消息监听,但是 redis stream 却没有相对于的组件

那么就我们就亲手搭建一个用于 redis stream 监听的注解吧(@RedisStreamListener)

项目地址:redisStreamListener: 专注于Redis流数据监听与处理的开源项目,提供高效、可扩展的解决方案,适用于实时数据处理和消息队列场景。

项目版本:springboot 2.6redis 5.0.14JDK 1.8

搭建 redis单机 和 redis cluster(windows环境)(有环境就不用看这部分了)

redis windows版 下载安装

本篇文章是使用 redis 5.0.14 的 windows 版来讲解的

搭建流程都差不多,配置文件也可以直接拿去用,只是配置目录密码这些需要自己调整

相信看这篇文章的大伙都对redis有很好的理解了,下载安装就不写了

一、redis 单机 搭建

单机启动很简单,下载的文件里面也有 conf 配置

这里就不展示 redis.windows.conf 文件了,直接用 server 客户端启动即可

bat 文件是windows的批处理文件,linux不可用哦

记得修改 bat 文件的路径这些哦

redis 单机启动脚本(redis-alone.bat)

bash 复制代码
@echo off
:: 设置Redis目录
set redis_catalogue=D:\Environment\Redis-x64-5.0.14.1
:: redis 密码
set redis_password=123456
:: redis.conf 路径
set redis_conf_catalogue=D:\Environment\Redis-x64-5.0.14.1\redis.windows.conf

start "Redis server" cmd /k "%redis_catalogue%\redis-server.exe %redis_conf_catalogue%"

二、redis cluster 搭建

新建文件夹 8201、8202、8203

用三个配置启动三个主节点

新建文件(redis.conf)

每一个文件夹都需要配置一个 redis.conf 文件,文件下面有,需要修改端口号哦

配置文件(redis.conf)

需要修改一下端口号信息哦

比如我的 8201 端口在 8201 目录下,那 8202 目录的配置就是 8202 端口了,以此类推

bash 复制代码
# 修改为后台启动
# daemonize yes
# 修改端口号
port 8201
# 指定数据文件存储位置
dir D:/Environment/redisClusterCof/8201/
# 开启集群模式
cluster-enabled yes
# 集群节点信息文件配置
cluster-config-file nodes-8201.conf
# 集群节点超时间
cluster-node-timeout 15000
# 去掉bind绑定地址
# bind 127.0.0.1 -::1 (这里没写错就是家#注释掉bind配置)
# 关闭保护模式
protected-mode no
# 开启aof模式持久化
appendonly yes
# 设置连接Redis需要密码123(选配)
requirepass 123456
# 设置Redis节点与节点之间访问需要密码123(选配)
masterauth 123456

redis cluster 启动脚本 (redisCluster .bat)

bat 文件是windows的批处理文件,linux不可用哦

记得修改 bat 文件的路径这些哦

bash 复制代码
@echo off
setlocal enabledelayedexpansion

:: 设置Redis目录
set redis_catalogue=D:\Environment\Redis-x64-5.0.14.1
:: redis 密码
set redis_password=123456

:: redis配置文件路径
set config_path=D:\Environment\redisClusterCof

:: redis配置文件名
set redis_cof_name=redis.conf

:: 定义集群端口列表 整体路径:D:\Environment\redisClusterCof\8201\redis.conf
set ports=8201 8202 8203

:: 遍历启动所有Redis集群节点
echo Starting Redis Cluster Nodes...
for %%p in (%ports%) do (
    set config_file=%config_path%%%p%redis_cof_name%
    start "Redis Port %%p" cmd /k "%redis_catalogue%\redis-server.exe !config_file!"
    timeout /t 1 /nobreak >nul
)
:: 等待所有节点启动完成
timeout /t 2 /nobreak >nul

:: 启动集群 --cluster-replicas 0 代表没有从节点 全是主节点
:: redis-cli --cluster create --cluster-replicas 0 127.0.0.1:8201 127.0.0.1:8202 127.0.0.1:8203
start "Redis CLI" cmd /c "%redis_catalogue%\redis-cli.exe  -a %redis_password% --cluster create --cluster-replicas 0 127.0.0.1:8201 127.0.0.1:8202 127.0.0.1:8203 --cluster-yes"

:: timeout /t 3 /nobreak >nul

:: 启动Redis CLI 如果没有就算了
:: start "Redis CLI" cmd /c "%redis_catalogue%\redis-cli.exe" -c -p 8201 -a %redis_password%

项目搭建(pom文件、application.yaml)

实现流程

就是利用 BeanPostProcessor 在 spring 的 Bean 注入前后可以操作的生命周期,在 Bean 注入结束后,遍历 Bean 获取到包含 RedisStreamListener 注解的方法,用一个新的线程去监听 redis 的 stream,由于没值会阻塞线程,所以可以直接使用 while (!Thread.currentThread().isInterrupted()) 这样也可以实现方法的中断

代码讲解

监听注解(@RedisStreamListener)

less 复制代码
/**
 * redis stream 监听注解 类似 @KafkaListener
 * 使用该注解 最好配置 JVM 参数一起启动 如下:
 * java -Dredisson.cluster.stream.broadcast-group-id=*** -Dredisson.cluster.stream.consumer-name=***-jar ***.jar
 */
@Target({ ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisStreamListener {
    /**
     * 监听容器工厂 Spring 的 Bean 名称
     */
    String containerFactory() default "";

    /**
     * 监听的 topic
     */
    String topic();

    /**
     * 使用默认值 为广播模式
     * 自定义值 为竞争消费
     */
    String groupId() default "";

    /**
     * 自动提交
     */
    boolean autoSubmit() default true;
}

springboot启动后开启监听(RedisStreamListenerProcessor)

BeanPostProcess 的作用简述:

BeanPostProcessor(Bean后置处理器)是 Spring 框架的核心扩展点之一,主要用于在 Bean 初始化前后 执行自定义逻辑,对 Bean 进行增强或修改。

这里就是通过 Bean 初始化后扫描到 @RedisStreamListener 注解所在的方法进行 消息监听

scss 复制代码
/**
 * RedisStreamListener 注解处理器
 */
@Component
@Slf4j
public class RedisStreamListenerProcessor implements BeanPostProcessor, ApplicationContextAware {

    public static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(@NotNull ApplicationContext applicationContext) throws BeansException {
        RedisStreamListenerProcessor.applicationContext = applicationContext;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, @NotNull String beanName) throws BeansException {
        // 获取所有方法
        Class<?> targetClass = bean.getClass();
        Method[] methods = targetClass.getDeclaredMethods();
        // 遍历所有方法
        for (Method method : methods) {
            // 判断是否是RedisStreamListener注解
            if (method.isAnnotationPresent(RedisStreamListener.class)) {
                // 获取注解
                RedisStreamListener annotation = method.getAnnotation(RedisStreamListener.class);
                try {
                    // 启动监听器
                    startConsumerWithHandler(bean, method, annotation);
                } catch (Exception e) {
                    log.error("自动启动监听器失败", e);
                    throw new RuntimeException(e);
                }
            }
        }
        return bean;
    }


    /**
     * 启动监听器
     * @param bean bean的类
     * @param method bean其中之一的方法 因为调用这个方法的方法遍历了 bean 的所有方法
     * @param annotation 方法上的注解
     */
    private void startConsumerWithHandler(Object bean, Method method, RedisStreamListener annotation) {
        // 获取注解参数
        String topic = annotation.topic();
        String groupId = annotation.groupId();
        String factory = annotation.containerFactory();
        boolean autoSubmit = annotation.autoSubmit();
        // 获取 RedissonClient 注解中的 containerFactory 参数可以自定义 RedissonClient 的注入,测试用例有写
        // 使用默认 就是使用主要的 RedissonClient 的 Bean (@Bean + @Primary)
        RedissonClient redissonClient;
        if (StringUtils.isNotBlank(factory)) {
            redissonClient = applicationContext.getBean(factory, RedissonClient.class);
        } else {
            redissonClient = applicationContext.getBean(RedissonClient.class);
        }
        // 创建 RedisStreamConsumer 对象
        RedisStreamConsumer redisStreamConsumer = new RedisStreamConsumer(redissonClient);
        // 启动监听器
        redisStreamConsumer.startProcessing(topic, groupId, autoSubmit, (map, ack) -> handleStreamMessage(bean, method, map, ack));
    }

    /**
     * 处理消息
     * 主要用于赋值 并执行方法体
     */
    private void handleStreamMessage(Object bean, Method method, Map.Entry<StreamMessageId, Map<String, Object>> messageMap, Acknowledgment ack) {
        try {
            method.setAccessible(true);
            // 获取 方法 的所有参数类型
            Class<?>[] parameterTypes = method.getParameterTypes();
            // 根据方法参数类型传递合适的参数
            Object[] args = new Object[parameterTypes.length];
            for (int i = 0; i < parameterTypes.length; i++) {
                Class<?> parameterType = parameterTypes[i];
                // 获取 值
                if (parameterType.equals(Map.class)) {
                    Map<String, Object> value = messageMap.getValue();
                    args[i] = value;
                }
                // 序列化
                if (parameterType.equals(String.class)) {
                    Map<String, Object> value = messageMap.getValue();
                    args[i] = JSONObject.toJSONString(value);
                }
                // 获取 id
                if (StreamMessageId.class.isAssignableFrom(parameterType)) {
                    StreamMessageId key = messageMap.getKey();
                    args[i] = key;
                }
                // 手动提交配置
                if (Acknowledgment.class.isAssignableFrom(parameterType)) {
                    args[i] = ack;
                }
            }
            // 执行方法
            // 就是通过方式 把参数传递给方法体
            method.invoke(bean, args);
        } catch (Exception e) {
            log.error("handleStreamMessage 处理消息失败: {}", e.getMessage(), e);
            throw new RuntimeException(e);
        }
    }
}

手动提交(Acknowledgment)

对于该接口后续的测试用例会讲解

csharp 复制代码
/**
 * 手动提交接口
 */
public interface Acknowledgment {
    // 提交
    void acknowledge();
}

消息消费类(RedisStreamConsumer)

这里就是我们进行监听消息并消费的地方

typescript 复制代码
/**
 * redis Stream 消费者
 */
@Slf4j
public class RedisStreamConsumer {

    private final RedissonClient redisson;

    private final static String LOCK = "redis:stream:lock:2dfe3f31";

    /**
     * 可以不用配置 但最好配置一下 可以更好的追踪
     * 广播模式得到的消费组 可以保存在机器上 然后通过 JVM 命令修改
     * 启动命令:java -Dredisson.cluster.stream.broadcast-group-id=*** -jar ***.jar
     */
    private final String BROADCAST_GROUP_ID;

    /**
     * 可以不用配置 但最好配置一下 可以更好的追踪
     * 消费者名称 每个 JVM 示例应该用不同的值 主要用于消费竞争
     * 没有被确认(ack)的消息 会存在 PENDING 列表中 通过 redis命令:xpending streamKey groupId 查看 这里面也保存了 消费者名称等信息
     * 启动命令:java -Dredisson.cluster.stream.consumer-name=*** -jar ***.jar
     */
    private final String CONSUMER_NAME;

    public RedisStreamConsumer(RedissonClient redisson) {
        this.redisson = redisson;
        Environment environment = RedisStreamListenerProcessor.applicationContext.getEnvironment();
        this.BROADCAST_GROUP_ID = environment.getProperty("redisson.cluster.stream.broadcast-group-id", "");
        this.CONSUMER_NAME = environment.getProperty("redisson.cluster.stream.consumer-name", "");
    }

    /**
     * 消费者线程 用于查看或者停止
     */
    private static final Map<String, Thread> consumerThreads = new ConcurrentHashMap<>();

    private static final String ip;

    static {
        try {
            ip = InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException e) {
            log.error("获取本机IP地址失败", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 启动消息处理 自动提交
     * topic 主题
     * groupId 消费者组 相同为负载均衡 不同为广播模式
     * messageProcessor 消息处理器
     * StreamMessageId 消息ID
     */
    public void startProcessing(String topic, String groupId, Consumer<Map.Entry<StreamMessageId, Map<String, Object>>> messageProcessor) {
        startProcessing(topic, groupId, true, (message, ack) -> messageProcessor.accept(message));
    }


    /**
     * 启动消息处理
     * topic 主题
     * groupId 消费者组 相同为负载均衡 不同为广播模式
     * autoSubmit 是否自动提交
     * messageProcessor 消息处理器
     * StreamMessageId 消息ID
     * Acknowledgment 自动提交类
     */
    public void startProcessing(String topic, String groupId, boolean autoSubmit, BiConsumer<Map.Entry<StreamMessageId, Map<String, Object>>, Acknowledgment> messageProcessor) {
        final String finalGroupId = this.judgmentGroupId(groupId);

        // consumerName 消费者名称 主要用于负载均衡
        String consumerName;
        if (StringUtils.isBlank(CONSUMER_NAME)) {
            consumerName = ip + ":" + UUID.randomUUID();
        } else {
            consumerName = ip + ":" + CONSUMER_NAME;
        }
        // 线程标识 主要用于停止
        String threadKey = topic + ":" + groupId + ":" + consumerName;
        if (consumerThreads.containsKey(threadKey)) {
            log.warn("Consumer already running for: {}", threadKey);
            return;
        }
        Thread consumerThread = new Thread(() ->
                processMessages(topic, finalGroupId, consumerName, autoSubmit, messageProcessor), "stream-consumer-" + threadKey);
        consumerThread.setDaemon(true);
        consumerThread.start();
        // 保存线程 用于查看或者停止
        consumerThreads.put(threadKey, consumerThread);
    }

    /**
     * 判断是否为广播模式
     */
    private String judgmentGroupId(String groupId) {
        // 如果不为空 则为负载均衡模式
        if (StringUtils.isNotBlank(groupId)) {
            return groupId;
        }
        // 如果为空 则为广播模式
        if (StringUtils.isBlank(BROADCAST_GROUP_ID)) {
            return ip + ":" + UUID.randomUUID();
        }
        return BROADCAST_GROUP_ID;
    }

    /**
     * 监听并处理消息
     */
    private void processMessages(String topic, String groupId,
                                 String consumerName, boolean autoSubmit, BiConsumer<Map.Entry<StreamMessageId, Map<String, Object>>, Acknowledgment> processor) {
        // 获取Redisson的RStream对象
        RStream<String, Object> stream = redisson.getStream(topic);


        // 创建消费者组(如果不存在)
        try {
            stream.createGroup(StreamCreateGroupArgs
                    .name(groupId)
                    .makeStream());
        } catch (Exception e) {
            log.debug("Consumer group might already exist: {}", e.getMessage());
        }

        while (!Thread.currentThread().isInterrupted()) {
            try {
                // 批量读取 10条 30秒超时
                Map<StreamMessageId, Map<String, Object>> messages =
                        stream.readGroup(groupId, consumerName,
                                StreamReadGroupArgs
//                                        .greaterThan(StreamMessageId.AUTO_GENERATED)
                                        .neverDelivered()
//                                        .noAck()
                                        .count(10)
                                        .timeout(Duration.ofSeconds(30L)));

                // 如果超时了还没有数据需要处理 这里就处理没有 ack 的数据
                if (messages == null || messages.isEmpty()) {
                    // 在 pending 不常见的情况下 加锁就有性能问题,这里查询了没有数据就不需要加锁了
                    List<PendingEntry> pendingEntries = stream.listPending(groupId, StreamMessageId.MIN, StreamMessageId.MAX, 100);
                    log.debug("pendingEntries: {}", pendingEntries);
                    if (pendingEntries == null || pendingEntries.isEmpty()){
                        continue;
                    }
                    RLock lock = redisson.getLock(LOCK);
                    try {
                        // 加锁 保证 只有一个实例 能消费到 为确认数据
                        if (lock.tryLock(1000, TimeUnit.MILLISECONDS)) {
                            // 处理没确认的数据 就是没有 ack 的 pending 数据
                            messages = stream.pendingRange(groupId, StreamMessageId.MIN, StreamMessageId.MAX, 100);
                        }
                    } finally {
                        // 确保锁被释放
                        if (lock.isHeldByCurrentThread()) {
                            lock.unlock();
                        }
                    }
                    if (messages == null || messages.isEmpty()) {
                        continue;
                    }
                }

                // 处理消息
                for (Map.Entry<StreamMessageId, Map<String, Object>> entry : messages.entrySet()) {
                    // 获取消息ID
                    StreamMessageId messageId = entry.getKey();
                    // 获取消息
                    Map<String, Object> message = entry.getValue();
                    try {
                        log.debug("开始处理消息: messageId={} message={}", messageId, message);

                        // 可以手动提交
                        Acknowledgment acknowledgment = new ConsumerAcknowledgment(stream, groupId, messageId);
                        // 处理消息
                        processor.accept(entry, acknowledgment);
                        // 是否手动提交
                        if (autoSubmit) {
                            // 确认消息
                            stream.ack(groupId, messageId);
                        }
                        log.debug("消息处理完成: messageId={}", messageId);
                    } catch (Exception e) {
                        log.error("处理消息失败: messageId={}", messageId, e);
                    }
                }
            } catch (Exception e) {
                log.error("处理消息时发生错误", e);
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException ex) {
                    throw new RuntimeException(ex);
                }
            }
        }
    }

    /**
     * 停止监听
     */
    public void stopProcessing(String topic, String groupId, String consumerName) {
        String threadKey = topic + ":" + groupId + ":" + consumerName;
        Thread thread = consumerThreads.remove(threadKey);
        if (thread != null) {
            thread.interrupt();
            log.debug("Stopped consumer: {}", threadKey);
        }
    }

    /**
     * 停止所有监听
     */
    public void stopAll() {
        consumerThreads.forEach((key, thread) -> thread.interrupt());
        consumerThreads.clear();
    }

    public Map<String, Thread> getConsumerThreads() {
        return RedisStreamConsumer.consumerThreads;
    }


    /**
     * 确认消息
     */
    private static class ConsumerAcknowledgment implements Acknowledgment {
        private final RStream<String, Object> stream;
        private final String groupId;
        private final StreamMessageId messageId;

        public ConsumerAcknowledgment(RStream<String, Object> stream, String groupId, StreamMessageId messageId) {
            this.stream = stream;
            this.groupId = groupId;
            this.messageId = messageId;
        }

        public void acknowledge() {
            stream.ack(groupId, messageId);
        }

    }
}

消息生产类(RedisStreamProducer)

typescript 复制代码
@Slf4j
public class RedisStreamProducer {

    private final RedissonClient redisson;

    public RedisStreamProducer(RedissonClient redisson) {
        this.redisson = redisson;
    }

    public RedisStreamProducer() {
        this.redisson = RedisStreamListenerProcessor.applicationContext.getBean(RedissonClient.class);
    }

    /**
     * 发布单一消息到Stream
     */
    public StreamMessageId publishMessage(String streamName, String key, String msg) {
        RStream<String, String> stream = redisson.getStream(streamName);

        // 新的添加消息方式
        StreamMessageId msgId = null;
        try {
            msgId = stream.add(StreamAddArgs.entry(key, msg));
        } catch (Exception e) {
            log.error("publishMessage 添加消息失败 streamName={} ", streamName, e);
            throw e;
        } finally {
            log.debug("publishMessage 发布消息 streamName={} key={} msg={} msgId={} ", streamName, key, msg, msgId);
        }
        return msgId;
    }

    /**
     * 发布带特定字段的消息
     */
    public StreamMessageId publishMessage(String streamName, Map<String, Object> fields) {
        RStream<String, Object> stream = redisson.getStream(streamName);

        StreamMessageId msgId = null;
        try {
            msgId = stream.add(StreamAddArgs.entries(fields));
        } catch (Exception e) {
            log.error("publishMessage 添加消息失败 streamName={} ", streamName, e);
            throw e;
        } finally {
            log.debug("publishMessage 发布消息 streamName={} fields={} msgId={} ", streamName, fields, msgId);
        }
        return msgId;
    }

    private static final ObjectMapper objectMapper = new ObjectMapper();

    /**
     * 发布带特定字段的消息
     */
    public <T> StreamMessageId publishMessage(String streamName, T fields) {
        RStream<String, Object> stream = redisson.getStream(streamName);

        StreamMessageId msgId = null;
        try {
            Map<String, Object> map = objectMapper.convertValue(fields, Map.class);
            msgId = stream.add(StreamAddArgs.entries(map));
        } catch (Exception e) {
            log.error("publishMessage 添加消息失败 streamName={} ", streamName, e);
            throw e;
        } finally {
            log.debug("publishMessage 发布消息 streamName={} fields={} msgId={} ", streamName, fields, msgId);
        }
        return msgId;
    }
}

reidsson配置(RedissonConfig )

里面包含两套配置,一套单机的配置一套集群配置

kotlin 复制代码
@Configuration
public class RedissonConfig {

    @Value("${redisson.alone.host}")
    private String host;
    @Value("${redisson.alone.port}")
    private Integer port;
    @Value("${redisson.alone.password}")
    private String password;
    @Value("${redisson.alone.database}")
    private Integer database;

    /**
     * 单机模式 主要的
     * 多实例情况下 @Bean 没有加 value 的时候 需要加 @Primary 注解
     */
    @Bean
    @Primary
    public RedissonClient redisson(){
        String redisAddress = "redis://" + host + ":" + port;
        // 配置
        Config config = new Config();
        config.useSingleServer().setAddress(redisAddress).setDatabase(database).setPassword(password);
        // 创建RedissonClient对象
        return Redisson.create(config);
    }


    @Value("${redisson.cluster.nodes}")
    private String clusterNodes;
    @Value("${redisson.cluster.password}")
    private String clusterPassword;

    /**
     * 集群模式
     */
    @Bean("redisson2")
    public RedissonClient redisson2(){
        // 配置
        Config config = new Config();
        config.useClusterServers()
                .addNodeAddress(Arrays.stream(clusterNodes.split(","))
                        .map(node -> "redis://" + node)
                        .toArray(String[]::new))
                .setPassword(clusterPassword);

        // 创建RedissonClient对象
        return Redisson.create(config);
    }
}

消息 监听/消费 测试类(RedisListener )

测试消息的消费

这里就可以看见注解的使用

比如

topic 一样 groupId 一样那么就是负载均衡的消费消息

topic 一样 groupId 不一样就是广播模式

typescript 复制代码
@Component
@Slf4j
public class RedisListener {

    /**
     * 负载均衡模式 map
     */
    @RedisStreamListener(topic = "map", groupId = "test-group")
    public void map(Map<String, Object> map) {
        log.info("map 接收到消息:map={}", map);
    }

    /**
     * 负载均衡模式 手动提交 并设置工厂为 redisson2 集群模式(这里是配置的不同bean的地方)
     */
    @RedisStreamListener(topic = "acknowledge", groupId = "test-group", autoSubmit = false, containerFactory = "redisson2")
    public void acknowledge(String json, Acknowledgment acknowledgment) {
        log.info("acknowledge 接收到消息:{}", json);
        acknowledgment.acknowledge();
    }

    /**
     * 广播模式 groupId
     * 由配置决定 redisson.cluster.stream.broadcast-group-id
     * 配置为空则取随机值用于广播模式
     */
    @RedisStreamListener(topic = "test")
    public void broadcastMessage(Map<String, Object> map) {
        log.info("broadcastMessage 接收到消息:{}", map);
    }
}

测试消息的新增(RedisStreamController)

less 复制代码
@RestController
@Slf4j
@RequestMapping("/streamProducer")
public class RedisStreamController {

    /**
     * 单机模式
     */
    @Autowired
    private RedissonClient redisson1;
    /**
     * 集群模式
     */
    @Autowired
    @Qualifier("redisson2")
    private RedissonClient redisson2;

    /**
     * 发送消息
     */
    @PostMapping("/send/json")
    public String send(@RequestBody User user) {
        RedissonClient redisson = redisson1;
        if (user.factory == 2){
            redisson = redisson2;
        }
        StreamProducer streamProducer = new StreamProducer(redisson);
        streamProducer.publishMessage(user.streamName, user);
        return "success";
    }


    /**
     * 测试数据
     */
    @Data
    public static class User {
        // 一些测试数据
        private String name;
        private Integer age;
        private String sex;

        // stream 流的名称 就是topic
        private String streamName;

        // 工厂
        // 1 redisson1 单机
        // 2 redisson2 集群
        private int factory = 1;
    }
}

测试

一、项目启动的 JVM 配置

ini 复制代码
-Dserver.port=9091
-Dredisson.cluster.stream.broadcast-group-id=9091
-Dredisson.cluster.stream.consumer-name=9091

二、启动两个项目

记得 JVM 的配置也需要修改

-Dserver.port=9091 -Dredisson.cluster.stream.broadcast-group-id=9091 -Dredisson.cluster.stream.consumer-name=9091

怎么启动两个项目可以看我这篇文章:idea实现同时启动两个相同服务但不同端口的项目,全图解_idea启动两个相同的服务-CSDN博客

三、负载均衡(消费竞争)模式

调用Controller的接口

发送消息,哪个服务都可以发,所以我用 9091 端口 或者 9092 端口都是一样的

查看日志

四、广播模式

调用Controller的接口

查看日志

五、手动提交(之前测试的两个都是自动提交)

这里只开9091端口看效果即可

这里我们先把 acknowledgment.acknowledge() 注掉看效果

typescript 复制代码
    /**
     * 负载均衡模式 手动提交 并设置工厂为 redisson2 集群模式(这里是配置的不同bean的地方)
     */
    @RedisStreamListener(topic = "acknowledge", groupId = "test-group", autoSubmit = false, containerFactory = "redisson2")
    public void acknowledge(String json, Acknowledgment acknowledgment) {
        log.info("acknowledge 接收到消息:{}", json);
//        acknowledgment.acknowledge();
    }

项目重新启动后,调用Controller接口

等30秒看日志

项目还有些其他功能,比如关闭监听等,可以自己去试试

相关推荐
I'm a winner2 小时前
【FreeRTOS实战】互斥锁专题:从理论到STM32应用题
数据库·redis·mysql
Knight_AL2 小时前
Spring Boot 的主要特性与传统 Spring 项目的区别
spring boot·后端·spring
黄俊懿2 小时前
【深入理解SpringCloud微服务】Gateway简介与模拟Gateway手写一个微服务网关
spring boot·后端·spring·spring cloud·微服务·gateway·架构师
用户2190326527353 小时前
别再到处try-catch了!SpringBoot全局异常处理这样设计
java·spring boot·后端
梁同学与Android3 小时前
Android ---【经验篇】阿里云 CentOS 服务器环境搭建 + SpringBoot项目部署(二)
android·spring boot·后端
gugugu.3 小时前
Redis持久化机制详解(一):RDB全解析
数据库·redis·缓存
用户2190326527353 小时前
SpringBoot自动配置:为什么你的应用能“开箱即用
java·spring boot·后端
陌路203 小时前
redis持久化篇AOF与RDB详解
数据库·redis·缓存
喵了几个咪3 小时前
开箱即用的 GoWind Admin|风行,企业级前后端一体中后台框架:kratos-bootstrap 入门教程(类比 Spring Boot)
spring boot·后端·微服务·golang·bootstrap