Redis Streams 发布订阅

Redis Streams(推荐,支持 ACK)

1. 添加依赖
复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2. 配置文件
复制代码
spring:
  redis:
    host: localhost
    port: 6379

app:
  redis:
    stream: order-events
    group: order-group
    consumer: consumer-1
3. 创建消费者组
复制代码
@Configuration
public class RedisStreamConfig {

    @Value("${app.redis.stream}")
    private String streamKey;

    @Value("${app.redis.group}")
    private String groupName;

    @Bean
    public CommandLineRunner createConsumerGroup(StringRedisTemplate redisTemplate) {
        return args -> {
            try {
                redisTemplate.opsForStream().createGroup(streamKey, groupName);
                System.out.println("消费者组创建成功: " + groupName);
            } catch (Exception e) {
                System.out.println("消费者组已存在: " + groupName);
            }
        };
    }
}
4. 生产者:发送消息
复制代码
@Service
@RequiredArgsConstructor
public class StreamProducer {

    private final StringRedisTemplate redisTemplate;

    @Value("${app.redis.stream}")
    private String streamKey;

    public void send(String orderId, String action) {
        Map<String, String> body = Map.of(
            "orderId", orderId,
            "action", action,
            "timestamp", String.valueOf(System.currentTimeMillis())
        );

        ObjectRecord<String, Map<String, String>> record = StreamRecords
                .newRecord()
                .ofMap(body)
                .withStreamKey(streamKey);

        RecordId recordId = redisTemplate.opsForStream().add(record);
        System.out.println("消息已发送, ID: " + recordId.getValue());
    }
}
5. 消费者:接收消息 + 手动 ACK 确认
复制代码
@Configuration
@RequiredArgsConstructor
public class StreamConsumerConfig {

    private final RedisConnectionFactory connectionFactory;

    @Value("${app.redis.stream}")
    private String streamKey;

    @Value("${app.redis.group}")
    private String groupName;

    @Value("${app.redis.consumer}")
    private String consumerName;

    @Bean
    public StreamMessageListenerContainer<String, ObjectRecord<String, Map<String, String>>> listenerContainer() {
        StreamMessageListenerContainerOptions<String, ObjectRecord<String, Map<String, String>>> options =
                StreamMessageListenerContainerOptions.builder()
                        .pollTimeout(Duration.ofSeconds(2))
                        .targetType((Class) Map.class)
                        .build();

        StreamMessageListenerContainer<String, ObjectRecord<String, Map<String, String>>> container =
                StreamMessageListenerContainer.create(connectionFactory, options);

        // 订阅消费者组,从上次消费位置继续
        container.receive(
                Consumer.from(groupName, consumerName),
                StreamOffset.create(streamKey, ReadOffset.lastConsumed()),
                this::handleMessage
        );

        container.start();
        return container;
    }

    /**
     * 消息处理 + 手动 ACK
     */
    private void handleMessage(ObjectRecord<String, Map<String, String>> message) {
        try {
            Map<String, String> body = message.getValue();
            System.out.println("收到消息: " + body + ", ID: " + message.getId().getValue());

            // ===== 业务处理逻辑 =====
            processOrder(body);

            // ===== 处理成功后手动 ACK 确认 =====
            Long ackCount = connectionFactory.getConnection().xAck(
                    streamKey.getBytes(),
                    groupName.getBytes(),
                    message.getId().getValue()
            );
            System.out.println("ACK 确认成功, 确认数量: " + ackCount);

        } catch (Exception e) {
            // 处理失败,不执行 ACK,消息会留在 Pending List 中,后续可重试
            System.err.println("消息处理失败: " + e.getMessage());
        }
    }

    private void processOrder(Map<String, String> body) {
        System.out.println("处理订单: " + body.get("orderId"));
    }
}
6. 处理 Pending 消息(失败重试)

未被 ACK 的消息会进入 Pending List,可以定时拉取重试:

复制代码
@Scheduled(fixedDelay = 10000)
public void retryPendingMessages() {
    // 读取 pending 消息
    List<MapRecord<String, Object, Object>> pending = redisTemplate.opsForStream().read(
            Consumer.from(groupName, consumerName),
            StreamReadOptions.empty().count(10),
            StreamOffset.create(streamKey, ReadOffset.from("0"))
    );

    if (pending != null) {
        for (MapRecord<String, Object, Object> record : pending) {
            try {
                // 重新处理
                processOrder(record.getValue());
                // 重新 ACK
                redisTemplate.opsForStream().acknowledge(streamKey, groupName, record.getId());
            } catch (Exception e) {
                System.err.println("重试失败: " + record.getId());
            }
        }
    }
}

消息丢失

1. Redis 进程崩溃 + 未开启持久化

Stream 的消息存储在内存中,如果 Redis 没有配置 AOF 或 RDB 持久化,进程挂掉后所有数据直接消失。

2. AOF 刷盘策略不够激进

默认的 appendfsync everysec 每秒刷盘一次,极端情况下(比如 Redis 在刷盘间隔内崩溃)可能丢失最近 1 秒内写入的消息。

3. 主从切换窗口

Redis 主从复制是异步的,主节点写入消息后还没来得及同步到从节点就挂了,故障切换后这部分消息会丢失。

4. 消费者处理成功但 ACK 失败

消息被消费后、ACK 发送前消费者崩溃,消息会留在 PEL(待确认列表)中被重新投递,导致重复消费而非丢失------但如果你业务侧没做幂等,效果上等同于"数据异常"。

相关推荐
hweiyu0014 小时前
Redis命令:MSETNX
redis·缓存
浅念-18 小时前
Redis基础详解:单线程模型、String与Hash数据结构
服务器·数据库·redis·sql·mysql·nosql数据库·nosql
张某布响丸辣18 小时前
附件下载的安全边界:路径白名单、NAS 哨兵文件与 Redis Lua 一次性令牌
java·redis·redis lua·nas哨兵
Wang's Blog19 小时前
Java 接入Redis: 使用Jedis操作Redis
java·服务器·redis
月临水21 小时前
Redis 缓存预热、穿透、击穿、雪崩
redis
重生之我是Java开发战士21 小时前
【Redis】初识Redis
redis
Wang's Blog1 天前
Java 项目实战: 外卖平台-数据库环境搭建与十一张表结构解析
java·服务器·redis
喜欢的名字被抢了1 天前
01-Redis 基础篇:从定位到 Key 管理
redis·教程
Anastasiozzzz1 天前
重新定义 Agent 基建:Redis 在现代 AI 与智能体系统中的工程实践
java·人工智能·redis·ai