---
url: /zh/concepts/event-log.md
description: 为什么每个限界上下文只有一份分区的事件日志、日志里的位置意味着什么，以及 Wow 为什么没有事件总线。
---

# 日志就是总线

Wow 服务知道的每一条事实，都是它事件日志里的一个条目。快照、投影、发布的事件流和数仓，都从日志读出来。本页说明日志是什么、怎样排序，以及为什么除了它没有别的地方可写。

## 为什么只写一处 {#why-one-write}

一个系统先追加事件、再把它发布到总线，就写了两次。如果在两次写之间崩溃，事件已经提交却永远不会发布，所有观察者都永久错过了它。outbox 和重试能缩小这个缺口，却合不上它。

Wow 的做法是只写一次。提交一条命令，就是对日志的一次原子追加。所有对这次变更作出反应的东西，都从自己保存的检查点开始读日志。观察者崩溃了，重启后从检查点接着读。提交与观察者之间不会丢东西，因为两者之间什么都没有。

## 每个限界上下文一份日志 {#one-log-per-bounded-context}

每个限界上下文有自己的日志，只属于这个上下文的服务。其他上下文看到的是它发布的东西，永远看不到日志本身（[上下文之间](./cross-context)）。

日志的一个条目就是一条事件流：一条命令产出的事件，加上它们周围的事实。

| 部分 | 内容 |
| --- | --- |
| 身份 | 聚合（上下文、聚合类型、租户和 ID）；这次提交产生的版本 |
| 事件 | 每个事件的名字、修订号和内容，按顺序 |
| 请求 | 产生这些事件的命令 ID、请求 ID 和命令类型 |
| 谁 | 记录这次提交的操作人、提交后聚合的所有者和空间，以及准入放行的请求事实 |
| 因果 | 对 Saga 发出的命令，记录链条的根命令和直接原因 |
| 时间 | 提交时间 |

在事实之外，条目还可以带传输段：完成一个未结束的等待所需的信息、要确认的预留、追踪上下文。传输段只为投递服务。领域代码和查询永远看不到它，迁移和重放会丢弃它。

## 版本 {#versions}

每个聚合都有版本。它的第一次提交是版本 1，之后每次提交恰好加一。日志由两个唯一键守护：

* **（聚合，版本）**：基于同一版本做出决定的两个写者，不可能都提交。后到的那个得到冲突，在新状态上重新决定。
* **（租户，请求 ID）**：同一个请求不能提交两次。用同一请求 ID 重试，得到的是第一次提交的结果。

让写入正确的是这两个唯一键，而不是单写者。见[写：从准入到追加](./write-path)。

## 分区 {#partitions}

日志分成固定数量的分区，默认 64 个。一个聚合总是落在同一个分区里：聚合的身份（包括租户）被哈希到 4,096 个槽位之一，再由槽位映射到分区。哈希是存储格式的一部分，所以在每个 JVM、每个版本上都一样。

分区数必须是能整除 4,096 的 2 的幂，日志里有条目之后就不能再改。它是吞吐的旋钮：一个分区内的提交逐个编号，所以一个分区的提交速度大约就是一次存储往返允许的速度，而各分区之间并行提交。

分区也是归属的单位。服务的每个节点通过存储里的租约拥有其中一部分分区。分区的属主执行该分区内聚合的命令，并在它上面运行订阅。

## 位置 {#positions}

在一个分区内，每个条目都有一个位置：条目提交时分配的序号。**位置的顺序，就是提交变得可见的顺序。**

正是这条规则让读取是安全的。提交时间或自增 ID 都做不到这一点：先拿到 ID 的事务可能最后才提交，按 ID 翻页的读者就会永久跳过它。因为位置跟随可见性，从某个位置开始读一个分区、再从看到的最后一个位置接着读的读者，不会漏掉任何东西，每次看到的序列也都相同。

唯一的顺序保证是按聚合的：同一个聚合的事件按版本顺序到达。不同聚合的条目，哪怕在同一分区、哪怕一个是另一个引起的，也没有读者可以依赖的顺序。

## 后端 {#backends}

日志合同没有可选部分：后端要么全部实现，要么就不是日志后端。一套测试套件让每个后端都遵守同一份合同。

| 后端 | 用途 |
| --- | --- |
| PostgreSQL | 首选。一条语句完成版本检查、条目编号和插入；冲突时整体回滚，不留空洞。 |
| MongoDB | 一等后端。用事务给条目编号；分区的属主批量提交，数据库看不到争用。需要副本集。 |
| 内存 | 测试和示例。 |

每个聚合类型有自己的日志表或集合，所以访问权限、变更数据捕获和重建都能按聚合进行。分区、位置和租约仍然按上下文计。同一上下文的不同聚合可以把日志放在不同的存储里。

## 格式 {#formats}

日志永久保存，所以它的格式是一份合同。格式版本按表记录，每个线上结构也带着自己的格式版本。增加一个带默认值的可选字段不算格式变化：旧读者忽略它，新读者填上默认值。在同一主版本内，新字段先由一个次版本开始读取，下一个次版本才开始写入，所以滚动升级期间相邻的两个次版本可以并存。旧读者处理不了的变化，要等下一个主版本和它的迁移工具。

你自己的事件以另一种方式变化：通过它们的名字、修订号和升级函数。见[决定与演化](./programming-model#events-change-over-time)。
