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(待确认列表)中被重新投递,导致重复消费而非丢失------但如果你业务侧没做幂等,效果上等同于"数据异常"。

相关推荐
范中勤5 小时前
LLM 动态加载与多用户缓存架构技术文档
redis·langchain·llm·缓存架构·多用户隔离
小小龙学IT6 小时前
Redis 源码深度解析:从常用命令到内部实现
redis·golang·开源
禾小西18 小时前
07丨Redis 哨兵机制:主库故障后,如何恢复服务?
java·开发语言·redis
骉马代驾20 小时前
代驾系统实战(三):抢单池的 Redis 锁 + RabbitMQ 异步消费怎么做
redis
行百里er21 小时前
Redis Streams——有确认、能回溯的消息队列
redis
小马同学-1 天前
Redis消息队列与客户端编程
数据库·redis
fengkai45451 天前
十二、Redis -2
运维·数据库·redis·容器
hweiyu001 天前
Redis命令:BLPOP
redis·缓存
敲个大西瓜1 天前
langchain笔记(一)
redis·笔记·langchain
小小龙学IT1 天前
C++ Redis 客户端深度解析(hiredis 与 redis-plus-plus)——工业数采实时缓存 C++ 侧实战
c++·redis·缓存