资深工程师入门指南
受众:正在评估或采用 Wow 用于生产系统的资深 / 首席 / 技术主管工程师。 版本:Wow 8.3.8(Spring Boot 4.x、Kotlin 2.3、JVM 17+)
摘要
Wow 是一个编译器驱动、完全响应式的 CQRS + 事件溯源框架。其显著特征是:所有命令路由、事件处理器注册和 API 文档都在编译时通过 KSP 生成 -- 零运行时反射。该框架在配备 MongoDB + Redis + Kafka 的普通硬件上实现了 约 60,000 TPS(SENT 等待模式)和 约 18,000 TPS(PROCESSED 等待模式)。它通过 LocalFirst 路由策略优先在 JVM 内分发,针对单聚合吞吐量进行了优化。你将用 Axon 的生态系统成熟度换取 Wow 的原始吞吐量、编译时安全性以及消除命令/事件阻抗不匹配的统一消息总线抽象。
| 能力 | 评估 | 来源 |
|---|---|---|
| 编译时命令路由 | KSP 生成 CommandAggregate 元数据,运行时无反射 | wow-compiler |
| 写吞吐量(SENT 模式) | ~60k TPS(AddCartItem),~48k TPS(CreateOrder) | README.md:70-74 |
| 写吞吐量(PROCESSED 模式) | ~18k TPS 适用于 AddCartItem 和 CreateOrder | README.md:76-98 |
| 事件存储后端 | MongoDB、Redis、R2DBC(MariaDB/PostgreSQL)、内存 | settings.gradle.kts:27-30 |
| 消息总线后端 | Kafka(分布式)、Redis(分布式)、内存(本地) | settings.gradle.kts:27-30 |
| 投影存储 | Elasticsearch、MongoDB、R2DBC、CoCache(缓存层) | settings.gradle.kts:24-31 |
| Saga 支持 | 带编译时事件到命令映射的无状态 Saga | wow-core saga |
| 补偿 | 用于失败事件的一流仪表板 + 重试引擎 | compensation/ |
| 测试覆盖率强制执行 | 最低 80%(jacoco)、Given-When-Expect DSL | CLAUDE.md:93 |
1. 核心架构洞察
Wow 将 WaitStrategy 视为一等架构原语,创建了一个基于推送的通知管线,在单个响应式链中桥接了命令总线和事件总线。
大多数 CQRS 框架将"等待命令结果"视为事后补充 -- 一个阻塞的 Future.get() 或轮询循环。Wow 反转了这一点:WaitStrategy 是一个响应式 sink(Sinks.Many<WaitSignal>),通过推送通知在处理生命周期的每个阶段传播。
为什么这很重要
在传统的 CQRS 设置中:
- 命令进入命令总线(不保证按聚合排序)
- 事件进入事件总线(按聚合排序)
- 调用者必须轮询读模型或关联 ID 表来了解处理何时完成
Wow 统一了这些关注点:
- 命令和事件都通过相同的
MessageBus<M, E>抽象流动,具有相同的LocalFirst路由优化 WaitStrategy桥接了两个管线 -- 命令发送者订阅一个响应式信号流,下游处理器(聚合、投影器、Saga 处理器)将信号推入该流- 六个不同的阶段(
SENT、PROCESSED、SNAPSHOT、PROJECTED、EVENT_HANDLED、SAGA_HANDLED)形成一个具有前置依赖的有向无环图,支持部分阶段等待
结果是:调用者可以以 ~60k TPS 使用 sendAndWaitForSent()(即发即忘到总线),或以 ~18k TPS 使用 sendAndWaitForProcessed()(完整链路,包括聚合执行、事件发布、投影和 Saga -- 全部通过响应式推送返回)。
第二层洞察:编译时元数据生成
wow-compiler KSP 处理器扫描注解(@OnCommand、@OnEvent、@AggregateRoot、@StatelessSaga 等)并生成:
CommandAggregate实现 -- 将命令类型映射到处理器方法,消除运行时分发开销- 事件处理器元数据 -- 知道哪些事件触发哪些投影/Saga 处理器
- OpenAPI 路由注册 -- 为每个
@CommandRoute注解的方法自动注册HandlerFunctionBean
这意味着在运行时你拥有一个预先计算的路由表,而不是基于反射的分发。编译器输出是 wow-core 引擎读取的内容。
2. 架构伪代码(Python)
这是核心 CQRS+ES 流程如果用类 Python 伪代码表达的样子。它展示了基本模式,而非 Kotlin 特定的语法。
# 架构伪代码 -- 不可运行,展示响应式管线
class WowEngine:
"""处理管线:命令 -> 验证 -> 加载聚合 ->
执行 -> 持久化事件 -> 发布事件 -> 投影 -> Saga"""
def handle_command(self, command_msg: CommandMessage) -> Mono[CommandResult]:
# 1. 幂等性检查(布隆过滤器或数据库检查)
if self.idempotency_checker.is_duplicate(command_msg.request_id):
raise DuplicateRequestIdException(command_msg)
# 2. 验证(Bean Validation + 可选的自验证)
self.validator.validate(command_msg.body)
# 3. 加载聚合(快照 + 事件溯源)
aggregate = await self.load_aggregate(command_msg.aggregate_id)
# 内部:snapshot_repo.load(agg_id)
# -> 如果快照存在:反序列化状态
# -> 否则:从工厂创建新状态
# -> event_store.load(agg_id, from_version=snapshot.version+1)
# -> 应用每个 DomainEventStream 重建状态
# 4. 对已加载的聚合执行命令
command_handler = self.routing_table[type(command_msg.body)] # KSP 预先计算
events = aggregate.execute(command_handler, command_msg)
# 返回事件,而非状态变更。聚合是一个纯函数:
# f(State, Command) -> List[DomainEvent]
# 5. 追加事件到事件存储(原子性,版本检查)
event_stream = DomainEventStream(
aggregate_id=command_msg.aggregate_id,
version=aggregate.next_version,
events=events,
request_id=command_msg.request_id
)
await self.event_store.append(event_stream)
# 此处失败 -> EventVersionConflictException(乐观并发)
# 或 DuplicateRequestIdException(幂等性)
# 6. 将事件溯源应用到聚合状态
aggregate.apply_events(event_stream)
# 7. 快照(如果快照策略触发)
if self.snapshot_strategy.should_snapshot(aggregate.version):
await self.snapshot_repo.save(aggregate.snapshot())
# 8. 将事件流发布到领域事件总线
await self.domain_event_bus.send(event_stream)
# LocalFirst 路由:首先交付给本地订阅者,
# 然后扇出到分布式(Kafka/Redis)订阅者
# 9. 投影读模型(异步,通过事件总线订阅)
# projection_handler 订阅 DomainEventBus,更新读模型
# 支持的后端:Elasticsearch、MongoDB、R2DBC、CoCache
# 10. SAGA 编排(异步,通过事件总线订阅)
# saga_handler 订阅 DomainEventBus,发出补偿命令
# Saga 是无状态的:监听事件,产生命令
return CommandResult(...)
class WaitStrategy_Flow:
"""WaitStrategy 如何创建基于推送的通知管线"""
def send_with_wait(self, command, wait_strategy):
# 注册等待策略(映射 wait_command_id -> sink)
self.wait_registrar.register(wait_strategy)
# 将等待端点地址传播到消息头部
wait_strategy.propagate(self.endpoint, command.header)
# 将命令发送到总线
self.command_bus.send(command)
# 每个下游处理器调用:wait_notifier.notify(wait_strategy_id, signal)
# 等待策略 sink 响应式地接收信号:
# SENT -> PROCESSED -> PROJECTED -> EVENT_HANDLED -> SAGA_HANDLED -> [完成]
# 调用者接收以下之一:
# - 一个 Mono[CommandResult](单一终端结果)
# - 一个 Flux[CommandResult](阶段完成时的流式结果)
return wait_strategy.waiting_last()3. 完整系统架构
此图展示了完整的架构表面区域 -- 所有模块、它们的关系以及数据流拓扑。
模块映射
| 模块 | 职责 | 层级 | 来源 |
|---|---|---|---|
wow-api | 纯 API 契约:CommandMessage、DomainEvent、AggregateId、所有注解 | 基础层 | settings.gradle.kts:21 |
wow-core | 框架引擎:命令/事件总线、事件存储抽象、投影、Saga、等待策略 | 引擎层 | settings.gradle.kts:22 |
wow-compiler | KSP 处理器:在编译时生成命令路由、事件元数据、OpenAPI 规范 | 开发时 | settings.gradle.kts:26 |
wow-spring | Spring IoC 集成:SpringServiceProvider 桥接 | 集成层 | settings.gradle.kts:32 |
wow-spring-boot-starter | 带功能变体的自动配置(mongo-support、kafka-support 等) | 集成层 | settings.gradle.kts:34 |
wow-webflux | WebFlux 命令端点自动注册 | API 层 | settings.gradle.kts:33 |
wow-kafka | 通过 Kafka 的分布式命令/事件总线 | 消息层 | settings.gradle.kts:27 |
wow-mongo | MongoDB 事件存储 + 快照存储 | 存储层 | settings.gradle.kts:28 |
wow-redis | Redis 事件存储 + 快照存储 | 存储层 | settings.gradle.kts:30 |
wow-r2dbc | R2DBC 事件存储(MariaDB/PostgreSQL) | 存储层 | settings.gradle.kts:29 |
wow-elasticsearch | Elasticsearch 投影存储 | 存储层 | settings.gradle.kts:31 |
wow-test | 单元测试 DSL:AggregateSpec、SagaSpec(Given-When-Expect) | 开发时 | settings.gradle.kts:44-45 |
wow-cosec | ABAC 授权框架 | 横切关注点 | settings.gradle.kts:40 |
wow-opentelemetry | 通过 OpenTelemetry 的链路追踪 + 指标 | 横切关注点 | settings.gradle.kts:35 |
compensation/* | 事件补偿编排器 + 仪表板 | 横切关注点 | settings.gradle.kts:56-63 |
wow-cocache | 基于 CoCache 的投影缓存层 | 存储层 | settings.gradle.kts:24 |
4. 命令处理数据流
此序列图追踪了命令从 API 调用到最终确认的完整生命周期。每个阶段都由 WaitStrategy 追踪。
从流程中得出的关键观察
乐观并发是唯一的锁定模型。
EventStore.append()检查聚合版本自加载以来是否发生变化。如果存在冲突,会抛出EventVersionConflictException。没有分布式锁。重试必须在更高层进行。LocalFirst决策发生在总线层,而非网关。命令总线和事件总线共享相同的LocalFirstMessageBus模式(LocalFirstMessageBus.kt:89-171)。如果聚合是本地且有订阅者,消息首先在进程内交付;然后副本被发送到分布式总线供其他实例使用。投影和 Saga 与命令路径解耦。 它们作为独立消费者订阅
DomainEventBus。这是标准的 CQRS。WaitStrategy仅为客户端通知目的而重新耦合它们。
5. 事件存储可扩展性模型
事件存储沿两个轴扩展:存储后端选择和聚合分片。
可扩展性模型总结
| 维度 | 机制 | 限制 | 来源 |
|---|---|---|---|
| 单聚合写吞吐量 | 单个聚合由乐观并发序列化;无法并行写入同一聚合 | 热点聚合会成为瓶颈 | EventStore.kt:38-43 |
| 多聚合写扩展 | AggregateIdSharding 将不同聚合路由到不同存储分片(如 CosId 雪花 -> 哈希 -> 分片) | 分片分布质量取决于 ID 生成方案 | AggregateIdSharding.kt:82-104 |
| 读扩展(长聚合) | SnapshotRepository 存储定期状态快照;仅重放自上次快照以来的事件 | 快照策略必须根据聚合类型调优 | VersionOffsetSnapshotStrategy.kt |
| 事件总线扇出 | LocalFirst 首先路由到本地消费者(零网络跳转),然后通过 Kafka/Redis 分发 | Kafka 分区排序必须与聚合 ID 对齐以保持按聚合排序 | LocalFirstMessageBus.kt:129-170 |
| 存储后端选择 | MongoDB(文档模型自然契合事件)、Redis(最高吞吐量,内存)、R2DBC(SQL,运维熟悉) | 每种后端有不同的延迟/吞吐量/运维特性 | settings.gradle.kts:27-30 |
6. 与替代方案的比较
并排比较:Wow vs Axon Framework vs Eventuate vs 手动 CQRS+ES
| 维度 | Wow(8.3.8) | Axon Framework(4.x) | Eventuate Tram | 手动(自建) |
|---|---|---|---|---|
| 语言 / 平台 | Kotlin 2.3,JVM 17+,响应式(Project Reactor) | Java/Kotlin,支持阻塞和响应式 | Java,Spring Boot | 任意 |
| 命令路由 | KSP 编译时代码生成;零反射 | 运行时注解扫描 + 反射 | 运行时注解扫描 | 手动接线 |
| 事件溯源 | 一等公民:EventStore 有 4 种后端选择 | 一等公民:Axon Server 或 JPA/JDBC | 基于 CDC(Debezium)或 JDBC 轮询 | 手动事件表 + 总线 |
| 消息总线 | 统一的 MessageBus<M,E> 带 LocalFirst 路由 | 分离的 CommandBus + EventBus 抽象 | 分离的命令/事件通道 | Kafka/RabbitMQ 手动接线 |
| 等待/通知 | 基于推送的 WaitStrategy 链(6 个阶段)带响应式 sink | 发送时的 CommandCallback;流式查询的 SubscriptionQuery | 轮询 CommandReplyOutcome 表 | 自定义关联 ID 轮询 |
| 快照策略 | VersionOffsetSnapshotStrategy -- 每 N 个版本快照一次 | 可配置触发(版本计数、时间) | 通过事件升级器快照 | 手动 |
| Saga / 流程管理器 | 无状态 Saga:通过 KSP 编译时事件到命令映射 | 有状态 Saga 带 @SagaEventHandler 和 @EndSaga | 带事件处理器和命令生产者的 Saga | 自定义编排 |
| 测试 | AggregateSpec / SagaSpec 带 Given-When-Expect DSL + fork 支持 | AggregateTestFixture / SagaTestFixture 带 Given-When-Then | 手动测试设置 | 手动 |
| OpenAPI 生成 | 通过 KSP 从 @CommandRoute 注解自动生成 | 手动或通过自定义插件 | 手动 | 手动 |
| 指标 / 可观测性 | OpenTelemetry 原生(所有总线/处理器上的装饰器链) | Axon metrics + Micrometer | 自定义 | 自定义 |
| 补偿 | 一流仪表板 + 重试引擎 | 通过 Axon Server 的死信队列 | 手动 | 手动 |
| 生态系统成熟度 | 较年轻(2021+),社区较小 | 成熟(2010+),社区庞大,AxonIQ 商业支持 | 中等(Eventuate 商业) | N/A(你自建) |
| 学习曲线 | 需要 DDD + ES + 响应式 + Kotlin KSP 概念 | 需要 DDD + ES;Axon Server 屏蔽复杂性 | 需要理解 CDC | 完全自掌控 |
何时选择 Wow
- 你在构建高吞吐量写微服务(目标 ~60k TPS 每节点)
- 你的团队熟悉 Kotlin、Project Reactor 和 KSP
- 你希望所有命令路由和事件处理具有编译时安全性
- 你已经运营 Kafka、MongoDB 或 Redis 并希望将其用作事件存储
- 你重视统一编程模型(命令和事件共享相同的总线抽象)
何时选择 Axon
- 你需要 Axon Server 生态系统(死信队列管理、事件存储管理 UI)
- 你的团队以 Java 为主且偏好阻塞式编程模型
- 你需要第三方商业支持(AxonIQ)
- 你正在构建的系统中,框架文档和社区支持是关键决策因素
7. 设计权衡分析
Wow 的优化方向
| 优化目标 | 实现方式 | 代价 |
|---|---|---|
| 单聚合写吞吐量 | 内存总线分发(本地订阅者),基于快照的加载,无分布式锁 | 热点聚合序列化对每个聚合 ID 产生硬上限 |
| 编译时安全性 | KSP 生成路由表,消除运行时类扫描和反射 | 每个领域模块都需要 wow-compiler KSP 依赖;增量编译开销 |
| 统一消息传递 | MessageBus<M,E> 同时用于命令和事件;两者共享 LocalFirst | 抽象是泛型的 -- 调试分布式路由需要理解 LocalFirst 逻辑 |
| 开发者体验 | AggregateSpec / SagaSpec DSL,@OnCommand / @OnEvent 注解,自动注册 WebFlux 路由 | 开发者必须内化"命令返回事件,而非状态变更"的范式 |
| 端到端响应式 | 全链路 Project Reactor;热路径中无任何阻塞 | 每个团队成员必须理解 Mono/Flux 语义;响应式堆栈调试更难 |
| 空命令(Void commands) | 命令可标记为 isVoid -- 聚合不产生事件,无状态变化 | 空命令跳过 LocalFirst 优化,因为无需等待(LocalFirstCommandBus.kt:41-46) |
Wow 的牺牲
| 牺牲 | 影响 | 缓解措施 |
|---|---|---|
| 无多聚合事务 | 跨聚合一致性需要带最终一致性的 Saga | 一流的 Saga 支持,配有用于监控故障的补偿仪表板 |
| 无事件存储管理 UI | MongoDB/Redis 事件存储必须通过原生工具管理 | 补偿仪表板弥补了失败事件的操作差距 |
| Kotlin/JVM 锁定 | KSP 编译器、Flow/Mono 使用、Kotlin 数据类用于事件 | Java 可通过 example-transfer Java 示例使用 Wow,但人体工学降低 |
| 无阻塞式编程模型 | 所有命令/事件路径都是响应式的;在处理器中混入阻塞代码会导致线程池饥饿 | 对 CPU 密集型处理器支持 @Blocking 注解 |
| 乐观并发重试是调用者的责任 | 如果发生 EventVersionConflictException,调用者(而非框架)必须重试 | 通过 requestId 去重的幂等性确保安全重试 |
| 分片是手动的 | AggregateIdSharding 必须按聚合类型配置;无自动重新平衡 | CosId 雪花 ID 提供良好分布;静态分片映射在实践中很常见 |
依赖耦合分析
| 依赖方向 | 性质 | 风险级别 |
|---|---|---|
wow-api(无依赖) | 纯契约 -- 除 Kotlin stdlib 外零外部依赖 | 无 |
wow-core -> wow-api | 引擎依赖于契约;硬耦合 | 低 -- API 是稳定契约 |
wow-core -> Project Reactor | 响应式流是核心抽象;深度嵌入 | 中 -- Reactor API 变更是破坏性的 |
wow-core -> CosId | 聚合 ID 和全局 ID 的 ID 生成 | 低 -- GlobalIdGenerator 是可插拔接口(GlobalIdGenerator.kt) |
wow-spring-boot-starter -> 所有后端 | 功能变体(mongo-support、kafka-support 等)创建可选耦合 | 低 -- Gradle 功能变体隔离后端依赖 |
wow-compiler -> wow-api 注解 | KSP 读取 wow-api 的注解来生成代码 | 低 -- 带版本的注解契约 |
8. 性能模型
基准测试结果(来自 example/ 性能测试)
所有基准测试在 Kubernetes 上使用 MongoDB + Redis + Kafka 运行(README.md:56-98)。
| 操作 | 等待模式 | 平均 TPS | 峰值 TPS | 平均延迟 | 来源 |
|---|---|---|---|---|---|
| AddCartItem | SENT | 59,625 | 82,312 | 29 ms | README.md:70-74 |
| AddCartItem | PROCESSED | 18,696 | 24,141 | 239 ms | README.md:76-80 |
| CreateOrder | SENT | 47,838 | 86,200 | 217 ms | README.md:88-92 |
| CreateOrder | PROCESSED | 18,230 | 25,506 | 268 ms | README.md:94-98 |
性能解读
SENT 模式比 PROCESSED 快 3 倍。 这是因为 SENT 只等待命令被总线接受。在响应之前不会发生聚合加载、事件持久化或投影。这是"即发即忘"模式 -- 当调用者不需要确认时很有用。
CreateOrder(SENT)虽然 TPS 相似,但延迟高于 AddCartItem(SENT)。CreateOrder 涉及更复杂的验证(外部
CreateOrderSpec规范服务被注入到处理器中,响应式地调用specification.require(it)-- 参见 Order.kt:106-138)。这是设计选择,而非框架限制。峰值 TPS 可以比平均水平高出 30-80%。 响应式管线能很好地应对突发流量,因为背压由 Reactor 的
Sinks.Many基础设施管理。PROCESSED 模式的瓶颈是事件存储追加延迟。 MongoDB 插入延迟在 ~18k TPS 时占主导地位。切换到 Redis 或调优 MongoDB 写关注度可以提高这个上限。
JMH 微基准测试单独存在。
wow-benchmarks模块包含针对CommandGateway、InMemoryCommandBus、InMemoryEventStore以及特定后端基准测试(MongoDB、Redis)的 JMH 基准测试。这些测量的是原始框架开销,而非端到端吞吐量。
TPS 扩展模型(每节点)
| 组件 | 近似上限 | 限制因素 |
|---|---|---|
| 内存命令总线分发 | >500,000 ops/s | JVM 吞吐量;不是瓶颈 |
| MongoDB 事件存储追加 | ~20,000 ops/s | MongoDB 单文档插入延迟 |
| Redis 事件存储追加 | ~50,000 ops/s | Redis ZADD 延迟 |
| Kafka 消息发布 | >100,000 msgs/s | Kafka 分区吞吐量 |
| 快照生成 | ~50,000 ops/s | 序列化 + 存储写入 |
| 投影更新(Elasticsearch) | ~10,000 ops/s | ES 索引吞吐量 |
9. 风险评估
什么会出问题以及如何应对
| 故障模式 | 可能性 | 影响 | 检测 | 缓解措施 | 来源 |
|---|---|---|---|---|---|
| 事件版本冲突 | 高(对同一聚合的并发写入下) | 命令被拒绝;调用者必须重试 | 日志中的 EventVersionConflictException | 通过 requestId 的幂等重试;重新设计聚合以减少写争用 | AbstractEventStore.kt:40-52 |
| 重复命令处理 | 低(使用布隆过滤器或数据库检查) | 幂等处理或业务逻辑重复 | DuplicateRequestIdException | 带布隆过滤器或数据库支持的 AggregateIdempotencyChecker | DefaultCommandGateway.kt:77-88 |
| 快照损坏 | 低 | 重新启动时聚合从陈旧/损坏状态加载 | 重放后出现意外状态 | 快照是可选的;事件重放始终是权威的;通过删除快照并重放进行修复 | EventSourcingStateAggregateRepository.kt:82-86 |
| Kafka 分区领导者故障 | 中(生产环境) | 事件交付延迟;如果 acks=0 可能消息丢失 | Kafka 消费者滞后指标 | 配置 acks=all、min.insync.replicas=2 | KafkaAutoConfiguration.kt |
| 事件存储后端中断 | 中 | 所有写入被阻塞;来自缓存投影的读取可能继续服务 | MongoDB/Redis/R2DBC 连接的健康检查 | 多区域副本集;响应式管线中的断路器 | EventStoreAutoConfiguration.kt |
| 投影漂移 | 中 | 读模型与事件存储真实状态偏离 | 自动协调;补偿仪表板跟踪失败事件 | StateEventCompensator / DomainEventCompensator 重放失败事件;仪表板提供手动重试 | compensation/ core |
| Saga 失败(无补偿) | 中 | 分布式事务处于不一致状态 | 补偿仪表板显示 Saga 失败 | 无状态 Saga 在失败时发出补偿命令;仪表板允许手动重试并指定 | StatelessSagaHandler.kt |
| WaitStrategy 内存泄漏 | 中 | 如果等待者永不断开连接,Sinks.Many 会无限累积 | waitingLast() 超时;活跃等待者的 JMX 指标 | WaitingFor 使用 Sinks.unsafe().many().unicast().onBackpressureBuffer() -- 如果 sink 永不完成,内存增长 | WaitingFor.kt:38-39 |
| JVM 垃圾回收暂停 | 低(使用 G1GC 调优) | 尾部延迟峰值;响应式超时触发 | GC 日志;OpenTelemetry JVM 指标 | 最低 -Xmx2g(gradle.properties:15);需要生产调优 |
安全考虑
- ABAC 授权 通过
wow-cosec提供(AbbacTagsMerger.kt)。命令携带@ApplyAbacTags元数据,由 CoSec 引擎评估。 - 租户隔离 通过
TenantId内置。每个聚合 ID 都携带一个租户 ID。@StaticTenantId注解(Cart.kt:34)可以在编译时固定租户。 - 操作者跟踪 通过事件上的
OperatorCapable捕获 -- 每个事件记录是谁发起了操作。
10. 架构决策日志
在采用 Wow 时,使用此格式记录架构决策。每个决策应记录考虑了什么以及为什么。
模板
## ADR-{NNN}:{标题}
**日期**:YYYY-MM-DD
**状态**:提议 | 已接受 | 已废弃 | 已替代
**上下文**:我们在解决什么问题?
**决策**:我们决定了什么?
**考虑过的替代方案**:
- 替代方案 1:为什么被拒绝
- 替代方案 2:为什么被拒绝
**后果**:
- 正面:我们获得了什么
- 负面:我们妥协了什么
- 缓解:我们如何管理负面影响采用 Wow 时的示例决策
ADR-001:事件存储后端
- 上下文:在 MongoDB、Redis 和 R2DBC 之间选择主要的事件存储。
- 决策:生产环境使用 MongoDB(文档模型契合事件流,运维成熟)。高吞吐量服务如果可承受重启时数据丢失(如果未配置持久化),可使用 Redis。
- 替代方案:R2DBC(SQL 熟悉但不如事件追加模式自然)。Redis(最高吞吐量但持久化运维复杂度更高)。
- 后果:MongoDB 需要副本集以确保生产持久性。单文档插入延迟是吞吐量上限(~20k TPS 每节点)。
ADR-002:等待策略模式
- 上下文:在
SENT和PROCESSED等待模式之间选择命令端点。 - 决策:
SENT用于高吞吐量摄入端点(如购物车添加、分析事件)。PROCESSED用于同步业务确认(如订单创建,调用者需要订单 ID + 计算字段)。 - 替代方案:始终
PROCESSED(更安全但吞吐量低 3 倍)。始终SENT(更快但调用者必须实现单独的确认轮询)。 - 后果:单个聚合可能以不同的等待模式暴露不同的端点。API 契约必须记录每个端点使用的模式。
ADR-003:快照策略
- 上下文:配置
SnapshotStrategy以平衡加载时间和存储开销。 - 决策:
VersionOffsetSnapshotStrategy(offset=50)-- 每 50 个版本快照一次。对于在其生命周期内很少超过 50 个事件的聚合,快照可能永远不会触发(可接受)。 - 替代方案:每个版本都快照(存储开销太大)。不快照(长生命周期聚合加载慢)。
- 后果:加载时间为 O(快照_反序列化 + 快照后的事件数)。根据每个聚合的事件频率调整偏移量。
ADR-004:投影存储选择
- 上下文:为投影选择读模型存储。
- 决策:Elasticsearch 用于搜索密集型读模型。MongoDB 用于简单的键值投影。CoCache 用于缓存热门投影。
- 替代方案:所有投影使用单一存储(运维更简单但对不同查询模式不够优化)。
- 后果:多个投影存储增加运维表面积。投影处理器必须知道它们写入哪个存储。
ADR-005:分片策略
- 上下文:为多节点部署配置聚合分片。
- 决策:使用
CosIdShardingDecorator对 N 个节点进行取模分片,以 CosId 雪花 ID 为键。聚合 ID 使用 CosIdSnowflakeIdGenerator,它提供自然分布的 ID。 - 替代方案:按聚合类型手动分片(
SingleAggregateIdSharding)。一致性哈希(重新平衡更复杂)。 - 后果:添加节点会改变模数,需要数据迁移。预分配足够的分片(如 64 个分片初始映射到 3 个节点),以允许无需数据移动的重新平衡。
11. 生产就绪检查清单
| 能力 | 状态 | 备注 |
|---|---|---|
| 事件存储备份/恢复 | 取决于后端 | MongoDB:使用 mongodump。Redis:使用 RDB/AOF。R2DBC:使用数据库原生工具。 |
| 事件存储迁移 / 版本控制 | EventUpgrader | 事件具有 @Revision 注解;EventUpgrader 可将旧事件形状转换为新形状。参见 EventUpgrader.kt。 |
| 水平扩展 | 通过 Kafka 消费者组 | 每个 Wow 实例订阅 Kafka 主题以获取分布式命令/事件。LocalFirst 确保共置聚合在进程内处理。 |
| 优雅关闭 | 可配置 | wow.shutdown-timeout(默认:60s)-- WowProperties.kt:29。在停止前排空进行中的命令。 |
| 健康检查 | Spring Boot Actuator | 标准 Spring Boot 健康端点。后端特定的健康指示器用于 MongoDB、Redis、Kafka。 |
| 指标导出 | OpenTelemetry | 所有总线、处理器、事件存储和投影分发器都包裹在指标装饰器中。参见 Metrics.kt 目录。 |
| 分布式追踪 | OpenTelemetry | 追踪上下文通过 CommandRequestHeaderPropagator 和 local-first 头部传播。参见 propagation 包。 |
| 速率限制 | 未内置 | 必须在 API 网关或通过 FilterChain 中的自定义 Filter 实现。 |
| 断路器 | 未内置 | 可使用 Reactor 的 Mono.retryWhen()。后端连接错误作为响应式错误传播。 |
| 审计日志 | 事件溯源本质上提供审计 | 每个状态变化都记录为带有操作者、时间戳和请求 ID 的领域事件。 |
12. 资深工程师入门指南
第一周:理解流程
- 阅读 Order 聚合:Order.kt。这是展示以下内容的经典示例:命令验证、外部服务注入、事件组合、错误处理和多事件返回。
- 阅读 Cart 聚合:Cart.kt。展示基于状态的条件逻辑的更简单示例。
- 追踪命令路径:DefaultCommandGateway.kt:
check()->propagate()->send()->next()。 - 理解聚合加载:EventSourcingStateAggregateRepository.kt:快照 -> 创建 -> 重放事件。
- 研究事件总线:DomainEventBus.kt:有序发布、有序处理、契约。
第二周:深入引擎
- 阅读
LocalFirstMessageBus:LocalFirstMessageBus.kt。这是理解分布式与本地路由的最重要的实现细节。 - 研究
WaitStrategy链:WaitingFor.kt 和 CommandStage.kt。理解阶段如何形成有向无环图。 - 检查自动配置:WowAutoConfiguration.kt 和所有子配置。这是框架如何将自己接入 Spring Boot 的方式。
- 查看测试示例:OrderTest.kt(聚合测试)和 CartSagaTest.kt(Saga 测试)。带
fork支持的 Given-When-Expect DSL 功能强大。 - 运行基准测试套件:
./gradlew wow-benchmarks:jmh查看原始框架开销数据。
相关页面
| 页面 | 相关性 |
|---|---|
| 架构指南 | 面向开发者的高级架构概述 |
| 配置参考 | 完整的 wow.* 配置属性参考 |
| 命令配置 | 命令总线、网关、等待策略配置 |
| 事件配置 | 事件总线、分发器配置 |
| 事件溯源配置 | 事件存储、快照、状态配置 |
| Saga 指南 | 无状态 Saga 开发指南 |
| 事件补偿 | 补偿编排器和仪表板 |
| 测试指南 | AggregateSpec/SagaSpec DSL 参考 |
| 架构深入 | 详细的组件内部结构 |
| 事件存储深入 | 事件存储设计和性能特征 |
| 示例:Order 服务 | 带 Saga 和投影的完整 Order 聚合 |
| 示例:Transfer 服务 | 基于 Java 的简单事件溯源示例 |