---
url: /zh/concepts/cross-context.md
description: 在 Wow 里，限界上下文之间怎样交换事件、命令、结果和读，以及每种工作该选哪种方式。
---

# 上下文之间

限界上下文的日志是它私有的。其他上下文从不读它，也从不写它。它们从它发布的事件流得知发生了什么，通过向它发命令请它做事。本页说明上下文之间通信的每一种方式，以及什么时候用哪一种。

## 选择方式 {#choosing-a-way}

| 你想 | 使用 | 发送方在何时完成 |
| --- | --- | --- |
| 对另一个上下文里发生的事作出反应 | 它**发布的事件流** | —（你是读者） |
| 请另一个上下文做事，然后继续自己的工作 | 它命令主题上的**命令**，带回执 | 命令被持久接收时 |
| 请另一个上下文做事，并立即拿到它的回答 | 对它内部端口的**同步调用** | 目标已提交或拒绝这条命令时 |
| 读另一个上下文的数据 | **类型化的查询客户端** | 查询已回答时 |

服务之间的异步消息只经过 Kafka。同步调用、读和等待信号发往对方服务的内部端口，以服务身份认证（[身份、租户与访问](./identity#the-internal-port)）。

## 发布的事件流 {#published-streams}

默认情况下，每个聚合的提交都作为一条流，发布在它自己的 Kafka 主题 `wow.<context>.<aggregate>.state` 上。流里的一条记录是一个**状态事件**：这次提交的事件流，加上提交之后聚合的状态。消费者随变更一起拿到状态，不必再去问上游服务。状态大到不便发送的聚合，可以只发布事件、不带状态；不应被外部看到的聚合，可以不发布。

事件在发布前升级到当前修订号，流的 schema 写在 Manifest 里。这条流是上下文的公开语言，由 Manifest 的兼容检查守住（[一份契约](./contract#compatibility)）。

订阅另一个上下文的流，与订阅服务自己的日志完全一样：一个处理函数、一个检查点、至少一次投递、停放、信号。处理函数分辨不出来源。

* 主题的分区由与日志相同的租约分配，而不是 Kafka 消费组；偏移量保存在服务自己的检查点存储里。每个节点、每个主题一个消费者，供给该主题上的所有订阅。
* 这里同样按聚合停放。主题不能按聚合读，所以台账记下聚合停下时的偏移量，恢复时从这个偏移量重扫那个分区，取出这个聚合的记录。Kafka 已不再保留其偏移量的条目无法恢复，只能跳过。
* 上游发布时不带状态，处理函数就声明可空的状态。期望从没有状态的流里拿到状态的服务不会启动。

上游上下文的 api jar 在类路径上时，处理函数可以直接接收上游自己的事件类。

## 发往另一个上下文的命令 {#commands-to-another-context}

发往另一个上下文的命令，写进那个上下文的命令主题 `wow.<context>.commands`，按与它的日志相同的方式确定键和分区。发送方（通常是 Saga）在 Kafka 持久接收命令后就完成了。它不等待目标，也不在目标的集群通道里。

目标像执行其他命令一样，执行从主题读到的命令：准入、按请求 ID 去重、决定、追加。因为 Saga 的命令有一个由触发它的提交推出的请求 ID，同一条命令发两次也只提交一次。

**拒绝会返回。** 拒绝是发送方需要知道的业务结果：库存没了，支付被拒了。命令带着回复的去处，目标在确认这条命令之前，把拒绝回执写进发送方上下文的回执主题 `wow.<context>.receipts`。发送方的 Saga 在它的拒绝处理函数里处理回执，并可以补偿。如果目标在中途崩溃，命令会再次投递、再次被拒、回执再次发送；Saga 按命令 ID 忽略重复的回执。Saga 也可以要求成功时同样发回执。

只有执行时的技术故障（而不是拒绝）才进入目标的失败台账。

当某个聚合类型经 Kafka 路由时，同一个主题也承载上下文自己的异步命令，让命令一旦被接收就是持久的。

## 同步调用 {#synchronous-calls}

有时发送方需要先拿到目标的回答才能继续：目标分配的编号、它的版本、它的拒绝。这时 Saga 通过框架的客户端（而不是手写的 HTTP）同步调用目标的命令网关：

* 调用发往目标服务的内部端口，以调用方服务的身份认证；
* 它带着由根命令推出的请求 ID，所以重试或重放的调用不会提交两次；
* 它带着根命令的作用域、操作人和请求事实，目标只在调用来自服务身份时才信任它们；
* 它可以带着等待，所以仍在等这条链的调用方能看到目标的各个阶段；
* 拒绝作为拒绝返回，不重试；得不到回答的调用让处理函数失败，处理函数在预算内重试，然后停放这个聚合。

Saga 声明它调用的命令，Manifest 列出它们。上下文之间的默认方式仍是带回执的命令主题。只在目标可用本来就是前提的地方才用同步调用。

## 类型化的客户端 {#typed-clients}

不是 Saga 的代码，例如服务的端点或定时任务，通过类型化的客户端发命令和查询：命令客户端和查询 API。对服务自己承载的上下文，它们在进程内运行，不需要编码命令。对另一个上下文，命令发往它的命令主题或经 HTTP 发往它的服务，查询经 HTTP 发送。两种情况下调用代码都一样。

远端那一侧来自对方上下文的契约，契约在它的 api jar 里。消费者只依赖这个 api jar，不写路径，也不写 HTTP 客户端代码。错误以类型化的错误码到达，并说明重试是否安全：查询只在因繁忙被拒（429）时重试，超时（504）绝不原样重试，流式查询从停下的地方续读。

## 契约 {#contracts}

这一切只需要对方上下文的 api jar。它带着上下文的契约，服务从类路径上读取它，用来在发送前检查命令、检查伸入对方上下文的等待计划，以及检查它订阅的发布的事件流。契约怎样进入 jar，见[一份契约](./contract#contracts-between-services)。

## 跨上下文等待 {#waiting-across-contexts}

等待可以沿着一条链进入其他上下文：订单已提交，然后支付 Saga 处理了它，再然后它发出的命令在库存上下文里提交。每一跳都直接向第一条命令来自的节点发信号。规则见[等待结果](./waits)。
