Redis:基于Java和Redis Stream实现发布订阅

一、Redis的Stream介绍

1.1 Redis Stream 是什么?

Redis Stream 是 Redis 5.0 版本引入的一种持久化的、有序的、可追溯的消息队列数据结构,专门用于处理消息发布 / 订阅(Pub/Sub)场景中消息丢失、无法回溯、不支持多消费者组等问题。

你可以把它理解成一个无限延伸的消息日志

  • 每条消息都有一个唯一的 ID(格式:时间戳-序列号,如 1710000000000-0
  • 消息按 ID 从小到大有序排列
  • 支持多消费者组同时消费,且每个消费者组有独立的消费进度
  • 消息持久化存储,重启 Redis 后数据不丢失

1.2 Stream 的核心特性与核心概念

1.2.1  核心概念
概念说明
消息 ID自动生成(XADD 命令)或手动指定,由毫秒时间戳 + 序列号组成,保证全局唯一且有序
消费者组(Consumer Group)一组消费者的集合,每个组有独立的消费游标(last-delivered-id),互不干扰
消费者(Consumer)隶属于某个消费组的具体消费实例,有唯一名称
Pending 列表(PEL)记录已分发给消费者但未确认的消息,防止消息丢失
1.2.2 核心命令(新手必知)

下面是最常用的 Stream 命令,附带简单示例:

(1)添加消息(XADD)

向 Stream 中添加消息,是最基础的写入操作:

redis

# 语法:XADD key [NOMKSTREAM] [MAXLEN | MINID [= | ~] threshold [LIMIT count]] *|id field value [field value ...]
# 示例:向 stream_myqueue 中添加一条消息,ID 由 Redis 自动生成
127.0.0.1:6379> XADD stream_myqueue * name "Alice" age 25
"1710000000000-0"  # 返回自动生成的消息 ID

# 手动指定 ID(需大于已存在的最大 ID)
127.0.0.1:6379> XADD stream_myqueue 1710000000000-1 name "Bob" age 30
"1710000000000-1"
(2)读取消息(XREAD)

非阻塞 / 阻塞式读取消息(独立消费,不依赖消费组):

redis

# 语法:XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] id [id ...]
# 示例1:非阻塞读取,从 ID 0-0 开始读取最多 2 条消息
127.0.0.1:6379> XREAD COUNT 2 STREAMS stream_myqueue 0-0
1) 1) "stream_myqueue"
   2) 1) 1) "1710000000000-0"
         2) 1) "name"
            2) "Alice"
            3) "age"
            4) "25"
      2) 1) "1710000000000-1"
         2) 1) "name"
            2) "Bob"
            3) "age"
            4) "30"

# 示例2:阻塞读取(BLOCK 0 表示永久阻塞),从最新 ID 开始等待新消息
127.0.0.1:6379> XREAD BLOCK 0 STREAMS stream_myqueue $
# 此时会阻塞,直到有新消息写入后返回
(3)创建消费组(XGROUP CREATE)

为 Stream 创建消费组(多消费者协作的基础):

redis

# 语法:XGROUP [CREATE key group id [MKSTREAM]] [SETID key group id] [DESTROY key group] [DELCONSUMER key group consumer]
# 示例:创建消费组 group1,从 ID 0-0 开始消费(从头消费)
127.0.0.1:6379> XGROUP CREATE stream_myqueue group1 0-0 MKSTREAM
OK

# 若想从最新消息开始消费,ID 用 $
127.0.0.1:6379> XGROUP CREATE stream_myqueue group2 $ MKSTREAM
OK
(4)消费组读取消息(XREADGROUP)

消费组内的消费者读取消息:

redis

# 语法:XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] id [id ...]
# 示例:消费者 consumer1 从 group1 中读取未消费的消息(> 表示读取消费组游标之后的消息)
127.0.0.1:6379> XREADGROUP GROUP group1 consumer1 COUNT 1 STREAMS stream_myqueue >
1) 1) "stream_myqueue"
   2) 1) 1) "1710000000000-0"
         2) 1) "name"
            2) "Alice"
            3) "age"
            4) "25"
(5)确认消息(XACK)

消费完成后确认消息,从 PEL 列表中移除:

redis

# 语法:XACK key group id [id ...]
# 示例:确认 group1 中 ID 为 1710000000000-0 的消息已消费完成
127.0.0.1:6379> XACK stream_myqueue group1 1710000000000-0
(integer) 1  # 成功确认的消息数

1.3 Stream 的典型使用场景

  1. 消息队列:替代传统的 Pub/Sub,支持消息持久化、回溯、多消费组,适合订单通知、日志收集等场景。
  2. 事件溯源:按时间顺序记录系统事件,可追溯任意时间点的状态。
  3. 实时数据流:结合 Redis 的高性能,处理实时产生的数据流(如物联网设备数据)。

1.4 与传统Redis pub/sub区别

1.5 总结

  1. Redis Stream 是 Redis 5.0 推出的持久化有序消息队列,解决了传统 Pub/Sub 消息丢失、无法回溯的问题。
  2. 核心特性包括:唯一消息 ID、多消费组、PEL 未确认消息列表、阻塞 / 非阻塞读取。
  3. 核心命令:XADD(写消息)、XREAD(独立读)、XGROUP(消费组管理)、XREADGROUP(消费组读)、XACK(确认消息)。

二、Java实现

2.1 依赖配置

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>
</dependencies>

2.2 自定义注解

import java.lang.annotation.*;

/**
 * Redis Stream 消息监听注解
 * 标记在方法上,用于自动注册 Stream 消费者
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface StreamListener {
    /**
     * Stream 名称(必填)
     */
    String streamKey();

    /**
     * 消费组名称(默认:stream_group_ + streamKey)
     */
    String groupName() default "";

    /**
     * 消费者名称(默认:stream_consumer_ + 随机数)
     */
    String consumerName() default "";

    /**
     * 每次拉取的消息数量
     */
    int batchSize() default 1;

    /**
     * 阻塞等待时间(毫秒,0 表示永久阻塞)
     */
    long blockTime() default 0;
}

2.3 RedisTemplate配置(序列化+Stream配置)

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * RedisTemplate 配置:指定序列化方式,保证 Stream 消息的序列化/反序列化正常
 */
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);

        // String 序列化器(key/HashKey 使用)
        StringRedisSerializer stringSerializer = new StringRedisSerializer();
        // JSON 序列化器(value/HashValue 使用)
        GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();

        // 设置 Key 序列化
        redisTemplate.setKeySerializer(stringSerializer);
        redisTemplate.setHashKeySerializer(stringSerializer);
        // 设置 Value 序列化
        redisTemplate.setValueSerializer(jsonSerializer);
        redisTemplate.setHashValueSerializer(jsonSerializer);

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

2.4 通用消息发布器(基于RedisTemplate)

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.stream.ObjectRecord;
import org.springframework.data.redis.connection.stream.StreamRecords;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

/**
 * 基于 RedisTemplate 的 Stream 通用发布器
 */
@Component
public class RedisStreamPublisher {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 发布任意类型消息到 Stream
     * @param streamKey Stream 名称
     * @param message 消息体(任意对象)
     * @return 消息 ID(格式:时间戳-序列号)
     */
    public <T> String publish(String streamKey, T message) {
        try {
            // 构建 Stream 消息记录(自动生成 ID)
            ObjectRecord<String, T> record = StreamRecords.newRecord()
                    .in(streamKey)          // 指定 Stream 名称
                    .ofObject(message);     // 消息体

            // 发送消息并获取自动生成的 ID
            String messageId = redisTemplate.opsForStream().add(record);
            System.out.println("消息发布成功,StreamKey:" + streamKey + ",消息ID:" + messageId);
            return messageId;
        } catch (Exception e) {
            throw new RuntimeException("发布 Stream 消息失败", e);
        }
    }
}

2.5 通用消息订阅处理器(基于RedisTemplate)

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.data.redis.connection.stream.*;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.stream.StreamListener;
import org.springframework.data.redis.stream.StreamMessageListenerContainer;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * 基于 RedisTemplate 的 Stream 订阅管理器
 * 扫描 @StreamListener 注解,自动创建消费组和消费者
 */
@Component
public class RedisStreamSubscriber implements ApplicationContextAware {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    private ApplicationContext applicationContext;

    /**
     * 初始化:扫描所有 @StreamListener 注解的方法,启动消费者
     */
    @PostConstruct
    public void init() {
        // 扫描所有带有 @StreamListener 注解的 Bean
        Map<String, Object> beans = applicationContext.getBeansWithAnnotation(StreamListener.class);
        for (Object bean : beans.values()) {
            for (Method method : bean.getClass().getDeclaredMethods()) {
                StreamListener annotation = method.getAnnotation(StreamListener.class);
                if (annotation != null) {
                    // 为每个注解方法启动独立的消费者
                    startConsumer(bean, method, annotation);
                }
            }
        }
    }

    /**
     * 启动单个消费者
     * @param bean 注解所在的 Bean 实例
     * @param method 注解标记的处理方法
     * @param annotation 注解参数
     */
    private void startConsumer(Object bean, Method method, StreamListener annotation) {
        // 解析注解参数
        String streamKey = annotation.streamKey();
        String groupName = annotation.groupName().isEmpty() ? "stream_group_" + streamKey : annotation.groupName();
        String consumerName = annotation.consumerName().isEmpty() ? "stream_consumer_" + UUID.randomUUID().toString().substring(0, 8) : annotation.consumerName();
        int batchSize = annotation.batchSize();
        long blockTime = annotation.blockTime();

        // 1. 创建消费组(不存在则创建)
        createConsumerGroupIfNotExists(streamKey, groupName);

        // 2. 配置消费者容器(核心:Spring Data Redis 提供的消息监听容器)
        StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, MapRecord<String, String, Object>> options =
                StreamMessageListenerContainer.StreamMessageListenerContainerOptions
                        .builder()
                        .batchSize(batchSize)                          // 每次拉取的消息数
                        .pollTimeout(Duration.ofMillis(blockTime))     // 阻塞等待时间(0 表示永久阻塞)
                        .targetType(HashMap.class)                     // 消息体类型
                        .build();

        // 3. 创建并启动监听容器
        StreamMessageListenerContainer<String, MapRecord<String, String, Object>> container =
                StreamMessageListenerContainer.create(redisTemplate.getConnectionFactory(), options);

        // 4. 注册消息监听器
        container.receive(
                Consumer.from(groupName, consumerName),                // 消费组 + 消费者
                StreamOffset.create(streamKey, ReadOffset.lastConsumed()),// 从上次消费的位置开始读取
                new org.springframework.data.redis.stream.StreamListener<>() {
                    @Override
                    public void onMessage(MapRecord<String, String, Object> record) {
                        try {
                            // 获取消息体和消息 ID
                            String messageId = record.getId().getValue();
                            Map<String, Object> payload = record.getValue();

                            // 反序列化消息体为目标类型(方法的第一个参数类型)
                            Class<?> paramType = method.getParameterTypes()[0];
                            Object message = redisTemplate.getValueSerializer().deserialize(
                                    redisTemplate.getValueSerializer().serialize(payload.get("__obj"))
                            );

                            // 调用注解标记的处理方法
                            method.invoke(bean, message);

                            // 5. 确认消息消费完成(ACK)
                            redisTemplate.opsForStream().acknowledge(streamKey, groupName, record.getId());
                            System.out.println("消息消费成功,ID:" + messageId + ",内容:" + message);
                        } catch (Exception e) {
                            System.err.println("处理 Stream 消息失败:" + e.getMessage());
                            // 可扩展:重试逻辑、死信队列等
                        }
                    }
                }
        );

        // 启动容器
        container.start();
        System.out.println("Stream 消费者启动成功:streamKey=" + streamKey + ", group=" + groupName + ", consumer=" + consumerName);
    }

    /**
     * 创建消费组(不存在则创建)
     */
    private void createConsumerGroupIfNotExists(String streamKey, String groupName) {
        try {
            // 尝试创建消费组(从 Stream 头部开始消费)
            redisTemplate.opsForStream().createGroup(streamKey, ReadOffset.from("0-0"), groupName);
        } catch (Exception e) {
            // 异常通常是消费组已存在,忽略即可
            System.out.println("消费组 " + groupName + " 已存在,无需重复创建:" + e.getMessage());
        }
    }

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

2.6 使用示例

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@SpringBootApplication
public class RedisStreamApplication {
    public static void main(String[] args) {
        SpringApplication.run(RedisStreamApplication.class, args);
    }

    // 示例消息实体
    static class OrderMessage {
        private String orderId;
        private String userId;
        private double amount;

        // 必须有默认构造函数(序列化需要)
        public OrderMessage() {}

        public OrderMessage(String orderId, String userId, double amount) {
            this.orderId = orderId;
            this.userId = userId;
            this.amount = amount;
        }

        // getter/setter
        public String getOrderId() { return orderId; }
        public void setOrderId(String orderId) { this.orderId = orderId; }
        public String getUserId() { return userId; }
        public void setUserId(String userId) { this.userId = userId; }
        public double getAmount() { return amount; }
        public void setAmount(double amount) { this.amount = amount; }

        @Override
        public String toString() {
            return "OrderMessage{" +
                    "orderId='" + orderId + '\'' +
                    ", userId='" + userId + '\'' +
                    ", amount=" + amount +
                    '}';
        }
    }

    // 消息发布示例
    @Component
    static class OrderService {
        @Autowired
        private RedisStreamPublisher publisher;

        // 测试发布消息
        public void createOrder() {
            OrderMessage orderMsg = new OrderMessage("ORDER_789", "USER_123", 199.9);
            publisher.publish("stream_order", orderMsg);
        }
    }

    // 消息消费示例(注解标记)
    @Component
    static class OrderMessageHandler {
        @StreamListener(
                streamKey = "stream_order",
                groupName = "order_group",
                batchSize = 2,
                blockTime = 0
        )
        public void handleOrderMessage(OrderMessage orderMessage) {
            // 业务处理逻辑
            System.out.println("【RedisTemplate 消费】收到订单消息:" + orderMessage);
        }
    }
}

2.7 配置文件(application.yml)

spring:
  data:
    redis:
      host: localhost
      port: 6379
      password:  # 无密码留空
      database: 0
      timeout: 10000

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值