上下文之间
限界上下文的日志是它私有的。其他上下文从不读它,也从不写它。它们从它发布的事件流得知发生了什么,通过向它发命令请它做事。本页说明上下文之间通信的每一种方式,以及什么时候用哪一种。
选择方式
| 你想 | 使用 | 发送方在何时完成 |
|---|---|---|
| 对另一个上下文里发生的事作出反应 | 它发布的事件流 | —(你是读者) |
| 请另一个上下文做事,然后继续自己的工作 | 它命令主题上的命令,带回执 | 命令被持久接收时 |
| 请另一个上下文做事,并立即拿到它的回答 | 对它内部端口的同步调用 | 目标已提交或拒绝这条命令时 |
| 读另一个上下文的数据 | 类型化的查询客户端 | 查询已回答时 |
服务之间的异步消息只经过 Kafka。同步调用、读和等待信号发往对方服务的内部端口,以服务身份认证(身份、租户与访问)。
发布的事件流
默认情况下,每个聚合的提交都作为一条流,发布在它自己的 Kafka 主题 wow.<context>.<aggregate>.state 上。流里的一条记录是一个状态事件:这次提交的事件流,加上提交之后聚合的状态。消费者随变更一起拿到状态,不必再去问上游服务。状态大到不便发送的聚合,可以只发布事件、不带状态;不应被外部看到的聚合,可以不发布。
事件在发布前升级到当前修订号,流的 schema 写在 Manifest 里。这条流是上下文的公开语言,由 Manifest 的兼容检查守住(一份契约)。
订阅另一个上下文的流,与订阅服务自己的日志完全一样:一个处理函数、一个检查点、至少一次投递、停放、信号。处理函数分辨不出来源。
- 主题的分区由与日志相同的租约分配,而不是 Kafka 消费组;偏移量保存在服务自己的检查点存储里。每个节点、每个主题一个消费者,供给该主题上的所有订阅。
- 这里同样按聚合停放。主题不能按聚合读,所以台账记下聚合停下时的偏移量,恢复时从这个偏移量重扫那个分区,取出这个聚合的记录。Kafka 已不再保留其偏移量的条目无法恢复,只能跳过。
- 上游发布时不带状态,处理函数就声明可空的状态。期望从没有状态的流里拿到状态的服务不会启动。
上游上下文的 api jar 在类路径上时,处理函数可以直接接收上游自己的事件类。
发往另一个上下文的命令
发往另一个上下文的命令,写进那个上下文的命令主题 wow.<context>.commands,按与它的日志相同的方式确定键和分区。发送方(通常是 Saga)在 Kafka 持久接收命令后就完成了。它不等待目标,也不在目标的集群通道里。
目标像执行其他命令一样,执行从主题读到的命令:准入、按请求 ID 去重、决定、追加。因为 Saga 的命令有一个由触发它的提交推出的请求 ID,同一条命令发两次也只提交一次。
拒绝会返回。 拒绝是发送方需要知道的业务结果:库存没了,支付被拒了。命令带着回复的去处,目标在确认这条命令之前,把拒绝回执写进发送方上下文的回执主题 wow.<context>.receipts。发送方的 Saga 在它的拒绝处理函数里处理回执,并可以补偿。如果目标在中途崩溃,命令会再次投递、再次被拒、回执再次发送;Saga 按命令 ID 忽略重复的回执。Saga 也可以要求成功时同样发回执。
只有执行时的技术故障(而不是拒绝)才进入目标的失败台账。
当某个聚合类型经 Kafka 路由时,同一个主题也承载上下文自己的异步命令,让命令一旦被接收就是持久的。
同步调用
有时发送方需要先拿到目标的回答才能继续:目标分配的编号、它的版本、它的拒绝。这时 Saga 通过框架的客户端(而不是手写的 HTTP)同步调用目标的命令网关:
- 调用发往目标服务的内部端口,以调用方服务的身份认证;
- 它带着由根命令推出的请求 ID,所以重试或重放的调用不会提交两次;
- 它带着根命令的作用域、操作人和请求事实,目标只在调用来自服务身份时才信任它们;
- 它可以带着等待,所以仍在等这条链的调用方能看到目标的各个阶段;
- 拒绝作为拒绝返回,不重试;得不到回答的调用让处理函数失败,处理函数在预算内重试,然后停放这个聚合。
Saga 声明它调用的命令,Manifest 列出它们。上下文之间的默认方式仍是带回执的命令主题。只在目标可用本来就是前提的地方才用同步调用。
类型化的客户端
不是 Saga 的代码,例如服务的端点或定时任务,通过类型化的客户端发命令和查询:命令客户端和查询 API。对服务自己承载的上下文,它们在进程内运行,不需要编码命令。对另一个上下文,命令发往它的命令主题或经 HTTP 发往它的服务,查询经 HTTP 发送。两种情况下调用代码都一样。
远端那一侧来自对方上下文的契约,契约在它的 api jar 里。消费者只依赖这个 api jar,不写路径,也不写 HTTP 客户端代码。错误以类型化的错误码到达,并说明重试是否安全:查询只在因繁忙被拒(429)时重试,超时(504)绝不原样重试,流式查询从停下的地方续读。
契约
这一切只需要对方上下文的 api jar。它带着上下文的契约,服务从类路径上读取它,用来在发送前检查命令、检查伸入对方上下文的等待计划,以及检查它订阅的发布的事件流。契约怎样进入 jar,见一份契约。
跨上下文等待
等待可以沿着一条链进入其他上下文:订单已提交,然后支付 Saga 处理了它,再然后它发出的命令在库存上下文里提交。每一跳都直接向第一条命令来自的节点发信号。规则见等待结果。