Redisson 的 RTopic 通过 Redis 原生 pub/sub 实现跨节点消息广播,依赖同一数据库、统一 Codec 和客户端本地线程池消费;不支持集群默认模式,无持久化、无 ACK、不保证顺序,适用于可丢失的轻量通知场景。

Redisson 的 RTopic 是怎么做到跨节点接收同一主题消息的
靠 Redis 的发布订阅原语 + Redisson 封装的客户端监听机制。Redis 本身不保存订阅关系,所有 RTopic 实例都通过底层 PUBLISH/SUBSCRIBE 与 Redis 通信,但 Redisson 在客户端做了两件事:一是把 Java 方法调用转成标准 Redis 协议命令;二是为每个 RTopic 维护一个本地线程池来持续消费 SUBSCRIBE 流。多个 JVM 进程里各自 new 出来的 RTopic,只要连的是同一个 Redis,就能同时收到 PUBLISH 过来的消息。
注意:这不是 Redisson 自己搞了一套消息总线,它完全依赖 Redis 的 pub/sub 模型,所以没有消息持久化、无 ACK、不保证顺序、断连期间消息直接丢——这些不是 bug,是 Redis 原生限制。
- 必须用同一个 Redis 数据库(
database配置一致),否则 SUBSCRIBE 到不同 db 就收不到 - 不要在单个
RTopic实例上调用多次addListener(),每次都会注册新监听器,导致重复执行 - 如果用的是 Redis 集群模式,
RTopic默认不可用(集群不支持 SUBSCRIBE),得切到单节点或开启 Redisson 的集群 pub/sub 代理模式(需配置useSingleServer或启用redisson.yaml中的subscriptionsPerConnection)
为什么 RTopic.publish() 后有些客户端收不到消息
最常见原因是连接隔离或序列化不一致。Redis pub/sub 只传原始字节,Redisson 默认用 JsonJacksonCodec 序列化,但如果不同服务用了不同 Codec(比如一个用 StringCodec,一个用 LongCodec),发出去的是字符串 "123",收端按 long 解就会报 ClassCastException 或静默失败。
- 所有用到同一主题的服务,必须使用完全相同的
Codec实例(推荐全局复用一个JsonJacksonCodec) - 检查 Redisson 客户端是否已初始化完成再调
getTopic(),未连接成功时publish()会抛RedisTimeoutException,但默认不打印堆栈 - 别在 Spring @PostConstruct 里直接 publish——此时 Redisson 可能还没完成订阅通道建立,建议加个简单健康检查(如
redisson.getNodesGroup().pingAll())
如何让订阅逻辑不阻塞主线程又避免内存泄漏
RTopic.addListener() 返回的是监听器 ID(long 类型),这个 ID 必须手动管理。Redisson 不会自动清理已注销的监听器,也不回收关联的线程资源。如果你在 Web 请求中反复 addListener 而不 remove,会累积大量空闲线程和闭包对象。
- 监听器函数体里别直接引用大对象(如整个 Service Bean),容易引发 GC 压力;用弱引用或只传必要字段
- 务必在合适的生命周期点调
topic.removeListener(listenerId),比如 Spring Bean 销毁时、Netty Channel 关闭后 - 如果只是临时监听(例如等某个异步任务完成),建议用带超时的
CountDownLatch+Future包裹,而不是长期 hold 住 listener
用 RTopic 替代消息队列有哪些实际代价
它轻量、低延迟,但放弃的是可靠性。你不能指望它替代 RabbitMQ 或 Kafka 来做订单通知或支付结果回调这类场景。
- Redis 重启 = 所有未消费消息清零,没有任何重放能力
- 网络抖动导致客户端 SUBSCRIBE 断开,重连后无法获取断连期间的消息(Redis 不存历史)
- 单条消息最大 512MB,但实际超过几 MB 就会影响 Redis 主线程性能,且序列化/反序列化成本陡增
- 当订阅者数量多(比如几百个实例),Redis 的 CPU 会明显上涨,因为每条
PUBLISH都要复制给每个活跃的 SUBSCRIBE 连接
真正适合的场景其实很窄:配置变更广播、缓存失效通知、轻量级状态同步——共性是消息可丢失、内容小、消费者能自我恢复。一旦业务开始要求“至少一次”或“顺序”,就得换方案了。










