一、前言:为什么 Redis Stream 是"专业级"消息队列?
在 Redis 5.0 之前,开发者只能用 List 或 Pub/Sub 实现简单消息传递,但它们存在明显短板:
- List:无 ACK、无法重试、不支持多消费者协同
- Pub/Sub:消息不持久、离线即丢
Redis Stream 的出现,彻底改变了这一局面!
它具备:
✅ 消息持久化
✅ 消费者组(Consumer Group)
✅ ACK 确认机制
✅ 消息回溯与重放
✅ 多播 + 负载均衡
本文将带你从零实现一个基于 Redis Stream 的高可靠异步任务系统,并对比其与 List/PubSub 的核心差异。
二、Stream 核心概念速览
| 概念 | 说明 |
|---|---|
| Stream | 类似 Kafka 的日志,每条消息有唯一 ID(如 1698765432123-0) |
| Consumer Group | 消费者组,组内多个消费者竞争消费 ,组间广播消费 |
| Pending Entries | 已读但未 ACK 的消息列表,支持失败重试 |
| ACK | 消费成功后确认,避免重复消费 |
📌 一句话理解 :
Stream = Redis 版的 Kafka(轻量级)
三、核心命令演示(Redis CLI)
1. 创建消息流
bash
# XADD stream_name * field value
> XADD order_stream * event "create_order" user_id "1001"
"1698765432123-0"
2. 创建消费者组
bash
# XGROUP CREATE stream group_name id [MKSTREAM]
> XGROUP CREATE order_stream order_group 0 MKSTREAM
OK
3. 消费者读取消息(组内)
bash
# XREADGROUP GROUP group consumer COUNT n STREAMS stream >
> XREADGROUP GROUP order_group worker1 COUNT 1 STREAMS order_stream >
1) 1) "order_stream"
2) 1) 1) "1698765432123-0"
2) 1) "event"
2) "create_order"
3) "user_id"
4) "1001"
4. 确认处理完成(ACK)
bash
# XACK stream group id
> XACK order_stream order_group 1698765432123-0
(integer) 1
5. 查看未确认消息(Pending)
bash
> XPENDING order_stream order_group
1) (integer) 0 # 未 ACK 消息数
2) "1698765432123-0"
3) "1698765432123-0"
4) 1) 1) "worker1"
2) "1" # 此消费者有 1 条 pending
四、Spring Boot 完整实战
步骤 1:添加依赖(Spring Data Redis ≥ 2.2)
XML
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
💡 确保 Redis 版本 ≥ 5.0
步骤 2:定义任务模型
java
public class OrderTask {
private Long orderId;
private Long userId;
private String requestId; // 幂等 ID
// getters/setters
}
步骤 3:封装 Stream 操作工具类
java
@Component
public class RedisStreamUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 发送消息到 Stream
*/
public String sendMessage(String streamKey, Map<String, String> message) {
return redisTemplate.opsForStream().add(
StreamRecords.newRecord()
.ofObject(message)
.withStreamKey(streamKey)
).getValue();
}
/**
* 从消费者组读取消息
*/
public List<MapRecord<String, String, String>> readMessages(
String streamKey, String groupName, String consumerName, int count) {
return redisTemplate.opsForStream().read(
Consumer.from(groupName, consumerName),
StreamReadOptions.empty().count(count).block(Duration.ofSeconds(1)),
StreamOffset.create(streamKey.getBytes(), ReadOffset.lastConsumed())
);
}
/**
* 确认消息已处理
*/
public void ackMessage(String streamKey, String groupName, String... messageIds) {
redisTemplate.opsForStream().acknowledge(groupName, streamKey, messageIds);
}
}
步骤 4:启动消费者线程(支持 ACK + 重试)
java
@Component
public class OrderTaskConsumer {
@Autowired
private RedisStreamUtil streamUtil;
private static final String STREAM_KEY = "order_task_stream";
private static final String GROUP_NAME = "order_group";
@PostConstruct
public void startConsumer() {
// 创建消费者组(幂等)
createGroupIfNotExists();
Thread consumer = new Thread(this::consumeLoop);
consumer.setDaemon(true);
consumer.setName("stream-order-consumer");
consumer.start();
}
private void createGroupIfNotExists() {
try {
redisTemplate.execute((RedisCallback<Void>) connection -> {
connection.streamCommands().xGroupCreate(
STREAM_KEY.getBytes(),
GROUP_NAME.getBytes(),
ReadOffset.from("0-0"),
true // MKSTREAM
);
return null;
});
} catch (Exception e) {
if (!e.getMessage().contains("BUSYGROUP")) {
log.error("创建消费者组失败", e);
}
}
}
private void consumeLoop() {
while (!Thread.currentThread().isInterrupted()) {
try {
List<MapRecord<String, String, String>> records =
streamUtil.readMessages(STREAM_KEY, GROUP_NAME, "worker-1", 10);
for (MapRecord<String, String, String> record : records) {
try {
// 解析消息
Map<String, String> data = record.getValue();
OrderTask task = parseTask(data);
// 处理业务
processOrder(task);
// ACK 确认
streamUtil.ackMessage(STREAM_KEY, GROUP_NAME, record.getId().getValue());
} catch (Exception e) {
log.error("处理消息失败, id={}", record.getId(), e);
// 不 ACK,消息会留在 Pending,后续可重试
}
}
if (records.isEmpty()) {
Thread.sleep(500); // 避免空轮询
}
} catch (Exception e) {
log.error("消费异常", e);
try { Thread.sleep(1000); } catch (InterruptedException ie) { break; }
}
}
}
private void processOrder(OrderTask task) {
// 扣库存、创建订单、发通知...
orderService.createOrder(task.getOrderId(), task.getUserId());
}
}
步骤 5:生产者发送任务
java
@Service
public class OrderService {
@Autowired
private RedisStreamUtil streamUtil;
public void submitOrder(Long orderId, Long userId) {
Map<String, String> message = Map.of(
"orderId", orderId.toString(),
"userId", userId.toString(),
"requestId", UUID.randomUUID().toString()
);
String messageId = streamUtil.sendMessage("order_task_stream", message);
log.info("订单任务已提交, messageId={}", messageId);
}
}
五、Stream 的核心优势总结
| 特性 | 说明 |
|---|---|
| 持久化 | 消息写入内存+磁盘,重启不丢 |
| ACK 机制 | 消费成功才删除,失败可重试 |
| 消费者组 | 支持水平扩展,自动负载均衡 |
| 消息回溯 | 可按 ID 查询历史消息(XRANGE) |
| 积压监控 | XPENDING 查看未处理消息 |
✅ 真正实现了"至少一次"语义(At-Least-Once Delivery)
六、适用场景 vs 不适用场景
✅ 推荐使用:
- 订单异步创建
- 优惠券发放
- 日志收集
- 用户行为分析
- 需要可靠处理的后台任务
❌ 不推荐:
- 超高吞吐(> 10万 QPS)→ 选 Kafka
- 复杂路由/事务 → 选 RabbitMQ
- 仅需广播 → 用 Pub/Sub 更轻量
七、结语
感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!