Redis Stream 本身不是事件溯源的完整实现,但提供了持久化、有序、可回溯、带消费者组和ACK的必要原语;落地关键在于规范事件结构(含event_id、aggregate_id、version等)、按领域命名Stream(如ecommerce.order.events)、启用PEL+手动ACK、读模型独立分组及业务层保障幂等与一致性。

Redis Stream 本身不是事件溯源(Event Sourcing)的完整实现,但它提供了构建它的**必要原语**:持久化、有序、可回溯、带消费者组和 ACK 的消息流。真正在微服务中落地事件溯源,关键不在于“用不用 Stream”,而在于**如何组织事件结构、如何管理读模型、如何处理重放与一致性边界**。
为什么不能直接把 XADD 当成事件存储?
很多团队一上来就 XADD events * type order_created data {"id":"123",...},然后发现很快踩坑:
- 事件 schema 混乱:不同服务写入的
type字段命名不统一,data结构嵌套深度不一致,下游解析失败 - 缺少元数据:没有
version、aggregate_id、causation_id,导致无法做幂等、无法追踪命令来源、无法重建聚合根 - Stream 名称随意:用
events单一流承载所有事件,失去按领域隔离能力,XTRIM策略无法差异化 - 没预留重放锚点:新服务上线时想从头重放,但 Stream 已被
XTRIM掉早期消息,或因MAXLEN限制丢失关键初始化事件
必须约定的事件结构与 Stream 命名规范
每个事件必须是结构化 JSON,且包含以下强制字段(由生产服务在 XADD 前注入):
-
event_id:UUID v4,全局唯一,用于去重和日志追踪 -
aggregate_type:如order、payment,用于路由到对应读模型更新器 -
aggregate_id:业务主键,如"ord_abc123",是事件溯源重建聚合根的索引键 -
version:整数,从 1 开始递增,同一aggregate_id下严格单调,用于乐观并发控制 -
occurred_at:ISO8601 时间戳(非 Redis 自动生成的 ID 时间),保证业务时间语义 -
causation_id:上游命令 ID(如 HTTP 请求 trace_id 或命令消息 ID),用于链路追踪 -
payload:纯业务数据,不嵌套额外 wrapper
Stream 名称应为 {domain}.{aggregate_type}.events,例如:ecommerce.order.events、finance.payment.events。这样既能按领域隔离,又支持细粒度 XTRIM MAXLEN ~1000000 控制保留策略,避免单一流无限膨胀。
消费者组必须启用 PEL + 手动 ACK,且重试逻辑不能绕过它
使用 XREADGROUP 读取时,必须搭配 NOACK 以外的模式——即默认行为。否则消息一旦被读取就从 Stream 中“消失”,失去重放能力。
- 消费者处理失败时,**绝不能直接丢弃消息**,而应调用
XACK失败后,让该消息留在 Pending Entries List(PEL)中 - 需另起一个监控协程,定期调用
XPENDING扫描 PEL 中超时(如 >5min)未确认的消息,触发告警或转入死信 Stream(如dlq.ecommerce.order.events) - 重放历史事件时,**不能用
XREAD从 $ 开始**,而应先用XINFO GROUPS查当前消费者组的last-delivered-id,再用XREADGROUP GROUP {group} {consumer} STREAMS {stream} 0强制从头拉取——这会把所有历史消息重新加入 PEL,后续仍走 ACK 流程 - 多个读模型(如订单搜索 ES、订单统计 DB)应各自创建独立消费者组,互不影响;共用组会导致一个模型卡住拖慢全部
重放与一致性边界必须由业务代码控制,Redis 不负责
Redis Stream 保证“消息不丢、顺序不变、可重复读”,但不保证“重放时读模型状态最终一致”。常见陷阱:
- 读模型更新器没做幂等:同一条
order_created事件被重放两次,ES 里出现两条相同订单文档 - 更新器没校验
version:重放旧版本事件覆盖了新状态(如重放version:1的创建事件,覆盖了已发货的version:3) - 跨聚合事件(如
order_paid)触发了对payment聚合的操作,但payment流还没重放完,导致状态错乱
所以,每个读模型更新器必须自带“事件指纹”去重表(如 processed_events:{aggregate_type} Hash,key=event_id,value=occurred_at),且在更新前检查 version 是否 ≥ 当前已存 version。跨聚合依赖则需靠 Saga 模式或最终一致性补偿,Redis Stream 本身不提供事务跨流能力。











