前言
现在很多 Java 项目里 Redis 已经是标配。缓存、分布式锁、计数器、排行榜这些都很常见。
Redis Stream 也经常被提到:既然项目已经有 Redis,那能不能顺手拿它做轻量消息队列?
我的看法是,可以,但要知道边界。Redis Stream 比 List + Pub/Sub 更适合做可靠一点的消息流,但它不是 Kafka,也不是 RocketMQ。它适合轻量异步、内部事件、小规模任务流,不适合所有消息场景。
它比 Pub/Sub 稳在哪里
Redis Pub/Sub 最大的问题是消息不会持久保存。订阅者不在线时,消息就错过了。
Redis Stream 不一样。它会把消息追加到 stream 里,每条消息有 ID,消费者可以按 ID 读取。配合 Consumer Group,还可以让多个消费者分摊处理消息。
这意味着它可以支持比较基础的可靠消费:
1.消息可以保留一段时间。 2.消费者可以从指定位置继续读。 3.消费组可以做任务分摊。 4.未确认消息可以重新处理。
对于一些内部小任务,这已经比 Pub/Sub 可靠很多。
Java 里怎么理解 Stream
可以把 Redis Stream 想成一个追加日志。生产者通过XADD追加消息,消费者通过XREADGROUP读取消息,处理完成后通过XACK确认。
如果用 Spring Data Redis,大概会接触到这些概念:
public record UserRegisteredMessage(
String userId,
LocalDateTime registeredAt) {
}
消息内容可以序列化成 Map 或 JSON。我的习惯是用统一的消息结构,不要让每个业务自己随便塞字段。
public record StreamMessage<T>(
String messageType,
String messageId,
T payload) {
}
这样消费者至少能知道自己拿到的是什么消息,也方便后续做版本管理。
消费组很关键
如果只是单消费者读 stream,那 Redis Stream 的价值还没有完全发挥出来。
Consumer Group 可以让多个消费者实例共同消费同一个 stream。每条消息通常只会分配给组内一个消费者处理,适合做任务分摊。
比如一个用户注册后的异步处理流:发送欢迎通知、初始化默认配置、同步 CRM。可以按不同职责建不同消费组。
但要注意,消费组之间互不影响。同一条消息会被每个消费组各自消费一次。这个特性很适合事件订阅,但如果你只是想多个实例共同处理同一类任务,就应该用同一个消费组。
消费组名称要稳定,不要每次启动随机生成。不然 Redis 会认为这是一个全新的消费组,历史消息处理位置也不一样。
Pending 消息不能没人管
Redis Stream 的可靠性很大一部分来自 Pending Entries List。
消费者读到消息后,如果没有XACK,这条消息会留在 pending 里。可能是消费者处理失败,也可能是消费者进程挂了。
如果没有专门机制处理 pending,消息就会卡在那里。表面上生产者还在写,消费者还在读,但某些消息已经没人管了。
所以使用 Redis Stream 时,一定要考虑 pending 重分配。可以定时扫描长时间未确认的消息,再认领给当前消费者处理。
这一步很容易被忽略。很多 demo 只写了读消息和 ack,没有写异常恢复。真实项目里,这部分不能省。
幂等仍然要做
Redis Stream 能提供确认机制,但不能帮你保证业务只执行一次。
消费者处理失败后,消息可能被重新投递。消费者处理成功但 ack 前宕机,消息也会被重新投递。
所以和其他消息队列一样,业务幂等还是要做。
比如发送优惠券,可以用业务流水号判断是否已经发过:
if (couponRecordRepository.existsByBizNo(message.messageId())) {
return;
}
不要把消息队列的投递语义误解成业务语义。中间件负责尽量把消息送到,业务系统负责知道重复来了该怎么办。
Stream 长度要控制
Redis 是内存数据库,虽然可以持久化,但内存成本仍然要认真看。
如果 stream 一直追加不清理,内存会不断增长。Redis Stream 支持按最大长度裁剪,比如只保留最近多少条。
但裁剪也不是随便设。保留太短,消费者稍微落后就可能读不到历史消息。保留太长,内存压力会上来。
我会按业务场景估算:
1.每秒消息量多少。 2.消息平均大小多少。 3.允许消费者落后多久。 4.异常时是否有其他补偿来源。
如果消息量很大,或者需要长时间回放,Redis Stream 就不太合适了。Kafka 这类日志型消息系统更适合。
不适合哪些场景
Redis Stream 不是不能用,而是要知道它不适合什么。
我不太建议用它承载这些场景:
1.超大吞吐事件流。 2.需要长时间保存和反复回放的日志。 3.跨团队、跨系统的大规模事件总线。 4.严格复杂的消息治理和权限隔离。 5.对消息堆积容量要求特别高的场景。
如果只是单体或小微服务内部的异步任务,Redis Stream 很方便。如果是核心交易链路的跨系统消息,还是要认真评估专业消息中间件。
和 List 队列比
很多老项目会用 Redis List 做队列,LPUSH加BRPOP。
List 队列简单,但缺少消费组、确认、pending 这些能力。消费者拿到消息后如果挂了,消息可能就丢了。要补可靠性,就要自己加很多逻辑。
Redis Stream 相当于把这些常见能力内置了一部分。所以如果已经决定用 Redis 做队列,我会优先考虑 Stream,而不是继续用 List 手搓。
当然,Stream 也更复杂。要管理消费组、pending、裁剪和监控。如果团队只是需要极简单的临时队列,List 也不是完全不能用。
监控看什么
Redis Stream 至少要看这些指标:
1.stream 长度。 2.消费组 lag。 3.pending 数量。 4.最老 pending 消息时间。 5.消费者实例是否活跃。 6.Redis 内存使用量。
其中最老 pending 时间很重要。它能告诉你是不是有消息长时间没人确认。单看 stream 长度,不一定能发现这个问题。
如果有管理后台,最好能按消费组看到当前 lag 和 pending。否则排查时只能临时敲命令,很不方便。
小结
Redis Stream 可以作为轻量消息队列使用,尤其适合 Java 项目里一些内部异步任务和小规模事件流。
但它不是万能替代品。使用前要把消费组、pending 恢复、幂等、stream 裁剪、监控这些问题想清楚。只写一个XADD和一个消费者循环,并不等于消息系统就可靠了。
我的判断是:如果项目已经有 Redis,消息量不大,保留时间不长,且主要是内部异步,Redis Stream 很值得用。如果消息系统已经成为核心架构能力,就应该上更专业的中间件。
喜欢的话,留下你的评论吧~