《Java 消息中间件·事件驱动架构》9.3 事件存储
事件存储是事件溯源的基础设施。本节比较自建事件存储与直接使用 Kafka 作为事件存储的权衡,指出 Kafka compacted topic 的局限,并用 Java 实测消费者把事件追加到本地 SQLite 日志表并重放到任意版本的完整流程。
tag
事件存储是事件溯源的基础设施。本节比较自建事件存储与直接使用 Kafka 作为事件存储的权衡,指出 Kafka compacted topic 的局限,并用 Java 实测消费者把事件追加到本地 SQLite 日志表并重放到任意版本的完整流程。
快照截断事件流以减少重建时的 apply 次数。本节讨论事件数阈值与时间窗口两种快照触发策略、快照与底层事件的版本一致性约束、快照失效时的降级回退方案与多版本保留策略,并用 H2 内存数据库实测 1000 条事件全量重建与快照恢复的性能对比与一致性验证。
事件溯源把状态变更历史作为唯一数据来源,本节厘清它与 CQRS + 投影的根本区别,给出适用场景、决策代价与三种常见反模式,讨论事件版本管理与外部系统交互策略,并用 H2 内存数据库演示从纯事件流重建聚合的完整过程与实测输出,佐证状态可复现性。
Kafka Streams 中 Stream enrichment(增强)的三种方式:join 做参照表 lookup、branch+merge 做条件分流重组、flatMap 做一对多展开。本节真跑 flatMap 增强拓扑,验证 1 条输入产生 2 条输出。
KTable-KTable 内连接与外键、KStream-KTable 左外连接的语义差异与常见错误。本机实测 KStream-KTable left join:key 不存在时输出 null 占位,对比 inner join 的数据丢失行为。
Kafka Streams 中 KStream(无界增量事件流)与 KTable(有界快照)的本质差异:同一个 topic 用不同算子消费,输出完全不同。本节真跑「stream -> table -> stream」拓扑,展示中间状态与最终输出。
Kafka Streams 的窗口操作是时间维度聚合的核心。本节通过编译运行 TimeWindowedKStream 拓扑,展示滚动窗口、跳跃窗口和会话窗口三种类型的边界行为与输出差异,讨论窗口大小与 topic retention 的耦合关系,并给出迟到数据的处理策略与性能影响分析。
Kafka Streams 的状态存储是本地 RocksDB 实例,与 changelog topic 配合实现故障恢复。本节通过编译运行带状态存储的拓扑,观察 state.dir 目录下的文件结构与大小,对比 KeyValueStore、WindowStore 和 SessionStore 三种类型,并给出状态丢失的重建策略与监控指标。
Kafka Streams 的拓扑模型是事件驱动系统的计算引擎。本节通过编译运行真实拓扑获取 Topology.describe() 输出,逐行解析 Source、Processor、Sink 三节点模型,展示有状态处理器的本地 RocksDB 存储结构与 changelog topic 自动创建机制,提供诊断命令与监控指标对照表。
消费者契约不是生产者的单元测试,而是消费者声明「我依赖这些字段」,再由 CI 验证生产者是否遵守。本文用纯 Java 实现契约断言,完整展示「生产者改了字段,测试红了」的四场景输出,并给出一个可嵌入 Maven verify 阶段的契约测试骨架。
Schema Registry 不是架构演进的银弹。本文讨论 Registry 的适用边界与三条限制,给出两种无 Registry 的替代方案——消息体内嵌版本信封与代码内版本路由表,并用 Jackson 实测新旧双版本并存解析,展示默认值的回填逻辑。
事件版本号放在主题名、消息信封还是字段里,每种方案在落盘、回放与消费端的成本各不相同。本文用 Java 实测三种版本化方案的兼容性判定逻辑,覆盖加字段、删字段、改名三类变更,按 FORWARD/BACKWARD/FULL 给出可复用的判定表。
乐观并发控制通过版本号解决多写者冲突。本节给出 CAS 风格版本号检查的可运行 Java 代码,模拟两个消费者同时写同一聚合根时的检测与重试流程,对比乐观锁与悲观锁的适用边界,包含重试退避策略、libraryd 并发修改场景与生产者幂等联动机制的深度分析。
幂等不是全局属性,而是应用层边界函数。本节深入讨论业务键、消息 ID、自然键三种幂等方案,给出可运行的对照判定代码,演练重平衡导致消息重发时的幂等防护机制,对比生产者幂等与消费者幂等的分界,附带去重表存储成本估算、排查清单与一致性注意事项。
Kafka 分区有序不等于业务有序也不等于因果有序。本机实验模拟乱序到达(COMPLETE→INIT→PAIR),对比时间戳排序与版本号排序两组策略,证明业务因果序必须由事件体自身携带单调版本号维护,时间戳因生产者时钟偏移与发送时序而不可信。
「最终一致」里的「最终」必须是一个可度量的分布,不是一句安慰。本文给出一致性窗口的度量代码:事件产生时打时间戳,下游写库提交后再打一个,按分位统计。本机两段负载实测显示窗口随积压线性增长,慢消费者下 p99 从 81ms 变成 4387ms。据此讨论补偿的触发条件、补偿失败的人工出口,以及一致性窗口该怎么承诺。
卷二讲过 outbox 这个模式,这一节讲它落地要写哪几张表、中继怎么保证至少一次、对账为什么必须三方比对。用 H2 内存库真跑一遍:六个借阅、四类差异(有库无事件、有事件无库、写侧内容不一致、读侧内容不一致),输出差异报告,再演示读模型重建能修复哪一类、修不了哪一类。
跨服务之后「事务」这个词只剩下一半含义。本文把 2PC、TCC、最终一致三种落地拆开:各自要求什么前提、把不确定性放在哪里、失败时留下什么残局,并用一张四维对照表(一致性强度、可用性、实现成本、失败模式)说明为什么大多数业务最终选了第三种,以及哪两类场景不该选它。
投影表是可丢弃的派生视图。本节真跑一次完整重建实验:先投影 20 条库存事件,清空表后从位移 0 重放重建并对比两次耗时与一致性,验证事件日志作为唯一事实来源的可复现性,深入讨论重建触发条件、快照优化策略、业务低峰期执行建议与常见注意事项。
投影是从事件流到读模型的单向转换。本节用 H2 内存表做投影目标,实测 Kafka 消费者消费事件进投影表的全过程,展示幂等写入、顺序处理、查询验证与错误恢复策略,并对比关系型数据库、Redis、Elasticsearch 等多种存储后端。