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