参考架构:事件驱动 / CQRS
在事件驱动架构中,服务不直接相互调用,而是将事件发布到事件日志(例如 Kafka),由消费者异步处理。这棵决策树从构建事件主干开始,逐步介绍事件溯源(从事件推导状态)、使用 CQRS 进行命令/查询分离、读模型投影、幂等性、对最终一致性的容忍,以及“exactly-once 投递”的谬误。目标是构建一个松耦合、可扩展、一致性取舍清晰的系统。
在事件驱动架构中,你面临的是哪个决策?
事件驱动系统的基础是一个通过异步事件连接各服务的持久化事件日志。你是仍在构建主干,希望从事件推导状态(事件溯源),还是正在分离读写路径(CQRS)?
- 我正在构建连接各服务的事件主干
- 我想从事件历史中推导状态
- 我想分离读写路径(CQRS)
在 CQRS 中如何构建读取端?你对一致性的容忍度如何?
在 CQRS 中,命令(写)端和查询(读)端是分离的;写入事件会更新为读模型提供数据的投影。关键问题是:你能否容忍读模型落后于写入(最终一致性),还是期望每个事件都恰好被处理一次?
- 我将通过投影从事件为读模型提供数据
- 读取必须是最新的;不能接受延迟
- 我希望事件被“恰好一次”处理
建议:持久化事件日志主干
事件驱动架构的核心是一个持久、有序的事件日志,生产者向其写入,消费者按各自的节奏读取。这一主干在时间和规模上实现了服务之间的解耦。
建议:事件溯源(从事件推导状态)
在事件溯源中,你存储的不是实体的当前状态,而是施加在其上的所有(不可变的)变更事件;当前状态通过按顺序应用这些事件推导出来。它提供完整的审计历史,以及回到过去任意时间点的能力。
建议:读模型投影
在 CQRS 中,写入端产生事件;投影监听这些事件,并更新针对查询优化的读模型(反规范化视图)。读取由这个现成的模型提供,与写入路径完全分离,两侧可以独立扩展。
建议:为容忍最终一致性而设计
在事件驱动/CQRS 系统中,读模型会略微落后于写入(最终一致性)。与其试图“消除”它,不如设计成能够容忍它;将少数需要严格即时一致性的流程单独处理。
建议:幂等性(exactly-once 的谬误)
在分布式消息传递中,端到端的“exactly-once 投递”在实践中是一个谬误;由于网络和重试,消息重复是不可避免的。正确的目标是幂等处理:处理同一事件两次的结果必须与处理一次相同。