构建纸牌系统:Pub/Sub层需持久化旧消息?是否应分离存储逻辑?
首先明确说:要求Pub/Sub层提供持久化能力完全合理——毕竟像你这种中途加入需要重放历史事件的场景,是实时系统里非常常见的需求,现在很多现代Pub/Sub组件本身就集成了持久化功能,完全能覆盖你的需求,没必要强行把存储和消息推送逻辑分离(除非你用的是纯临时消息的老式Pub/Sub)。
下面结合你的具体场景,给你拆解几个可行的方案:
方案1:用Redis Stream替代传统Redis Pub/Sub
你之前用的Redis Pub/Sub是纯临时的,但Redis 5.0推出的Redis Stream刚好解决了这个问题——它是带持久化的消息队列/流,天生支持:
- 消息持久化到磁盘,重启Redis也不会丢失
- 按消费者组管理订阅,新玩家加入时可以直接读取某个牌桌的全部历史消息
- 支持按消息ID、时间范围查询历史事件
具体实践思路:
- 为每个牌桌创建一个独立的Stream(比如
game:table:123) - 当有出牌、发牌等事件时,用
XADD把事件写入对应牌桌的Stream - 老玩家通过消费者组订阅新消息,实时推送给SSE客户端
- 新玩家加入时,先调用
XREAD读取该Stream的所有历史消息(可以指定从起始ID0-0开始),重放给客户端同步状态,然后再加入消费者组订阅新消息
这个方案的优势是部署简单(你已经在用Redis了,不需要额外引入新组件),学习成本低,完全能满足你对事件重放和统计查询的需求——统计时直接从Stream读取历史事件即可,既然你说统计对速度无要求,完全够用。
方案2:用Kafka实现全链路持久化与多场景消费
如果你预计游戏用户量会很大,或者未来有更复杂的消息处理需求,Kafka是更合适的选择:
- Kafka的消息天生持久化,可配置保留时间(比如保留7天或永久)
- 你可以按牌桌ID作为消息的Key,让同一个牌桌的事件落到同一个分区里,这样新玩家加入时,可以精准地从该分区的起始偏移量开始消费,重放该牌桌的全部历史事件,完全满足你需要的粒度
- Kafka支持多消费者组:一个消费者组负责实时推送SSE消息,另一个消费者组专门处理统计分析(甚至可以直接从Kafka读取历史数据做统计,不需要额外存储)
Kafka的优势是高吞吐量、高可靠性,适合大规模场景,唯一的缺点是部署和维护比Redis复杂一点,但如果你有团队能维护,它是非常稳妥的选择。
为什么不推荐分离Pub/Sub和存储?
你提到“单独部署消费者来存储事件”,这个方案确实会增加复杂度:
- 你需要维护额外的消费者服务,增加故障点(比如消费者挂了,事件就存不上了)
- 还要处理消息重复、幂等性问题(比如消费者重启后重复消费)
- 实时推送和历史查询需要分别对接Pub/Sub和存储系统,增加开发成本
而用Redis Stream或Kafka这类自带持久化的Pub/Sub组件,能把这两个逻辑合并,减少系统复杂度和故障风险。
针对统计需求的补充
不管用Redis Stream还是Kafka,统计旧事件都很方便:
- Redis Stream可以用
XREVRANGE按时间范围查询历史事件 - Kafka可以用命令行工具或者客户端API,读取指定分区、指定时间范围的消息
如果以后统计需求变复杂(比如需要做多维分析),再把数据同步到专门的分析数据库(比如ClickHouse)也不迟,初期直接用Pub/Sub组件的持久化能力完全够用。
总结一下:你的需求完全不需要强行拆分存储和Pub/Sub,选Redis Stream(中小规模)或Kafka(大规模)就能同时解决实时推送、事件重放、统计查询三个核心问题,是最简洁高效的方案。
内容的提问来源于stack exchange,提问作者Dudo

