这篇前言解决什么问题
序讲的是“为什么写”,前言讲的是“怎么用”。具体包括:
- 卷三在卷二之后接什么,哪些问题留给卷四;
- 13 章如何分成四段,每段解决什么问题;
- arch-lab 在卷三新增哪些组件,放在哪个目录,依赖方向是什么;
- 代码按 L1–L4 如何分级,卷三承诺到哪一级;
- 代码块里的
file、tier、verify标记怎么读、怎么抽取; - S1、S2、S3 三组固定实验场景分别验证什么;
- 实测值、目标值、示例值如何区分;
- 顺序阅读和带着一次跨库改造来读,两条路线怎么走。
如果只想尽快读正文,可以直接跳到 1.1,遇到代码标记再回来查。但如果你打算把书里的实验真正跑一遍,建议先花十分钟读完这篇。卷三的实验比卷二多一层“节点”概念,不先理解模拟器和场景,容易把单进程测试误当成分布式验证。
卷三在卷二之后接什么
arch-lab 仍然是那个 Go 订单系统:订单、库存、支付三个核心聚合,状态机仍是 CREATED → INVENTORY_RESERVED → PAID → FULFILLED,分支仍是 CANCELLED 和 REFUNDED。卷一确定了模块边界、端口与适配器、数据所有权和部署单元;卷二在边界内增加了度量、容量、缓存、异步、隔离、限流、超时重试、发布与观测。
卷三不重新设计这些边界,而是把系统从一个可部署单元推到多个节点:
- 数据不再只有一份,出现主库、备库、分片和副本;
- 调用不再只有本进程,出现跨节点往返、超时和结果未知;
- 时间不再只有一个来源,出现漂移、回拨和逻辑时钟;
- 事务不再只覆盖一个本地库,出现 2PC、Saga、TCC 和 Outbox;
- 故障不再只是进程退出,出现分区、丢包、重复和脑裂;
- 运维不再只是发布和回滚,出现切换、演练、回切和多活。
卷二末尾的进入卷三 已经列出了交接清单。卷三会逐条处理,并在最后一节把仍未证明的假设交给卷四。
卷三不包含:
- 服务网格、平台工程、完整混沌平台的建设;
- Kubernetes、CI/CD、监控系统的部署操作;
- 具体中间件的参数调优手册;
- 组织绩效、预算审批和团队重组;
- 生产级 Raft、分布式数据库内核的完整实现。
这些不是“不重要”,而是与卷三的核心问题(跨节点后业务不变量如何守住)关系较弱,或者需要真实生产环境才能讨论。卷三只在必要处提到它们,并说明边界。
13 章的四段结构
卷三共 13 章、39 节正文。按问题层次分成四段:先建立分布式语言,再处理复制与分区,然后进入事务与可靠交付,最后做协调、容灾和综合实验。
| 段 | 章节 | 主题 | 读完后应能 |
|---|---|---|---|
| 第一段 基础与模型 | 第 1–2 章 | 分布式问题迁移、故障模型、远程调用假设、物理时钟、逻辑时钟、混合时钟 | 说清哪些单机结论失效,以及时间与顺序如何编码 |
| 第二段 复制、分区与一致性 | 第 3–6 章 | 单主/多主/无主复制、Raft、分片与再平衡、一致性模型、会话保证、业务不变量 | 为一个订单系统选择复制与分片方案,并写出它的一致性承诺 |
| 第三段 事务与可靠交付 | 第 7–9 章 | 2PC/3PC、Saga、TCC、Outbox、消费幂等、消息顺序与死信、幂等键、对账、冲正 | 设计一条跨库下单链路,并说明每个断点如何收敛 |
| 第四段 协调、容灾与综合 | 第 10–13 章 | 分布式 ID、分布式锁、调度、多活、容灾切换、分片改造、跨库一致性验证 | 把前面的机制放进一个可演练、可回滚的订单系统 |
四段之间不是严格线性。第 6 章的一致性模型会回头解释第 3–5 章的复制与分片承诺;第 13 章的综合实验会同时用到第 7–12 章的组件。第一次读可以先按顺序建立语言,第二次再按问题跳转。
每章三节,完整的节标题、依赖图和阅读路径见目录与学习路径 。
arch-lab 在卷三新增什么
卷三沿用卷一、卷二的项目,不另起炉灶。新增组件集中在 internal/platform/,业务包仍然只通过端口依赖平台能力。下面是卷三扩展后的目录示意,不是要求读者一次建全。
archlab/
go.mod module archlab,go 1.26.0
cmd/orderd, cmd/simctl 服务入口与 S1/S2/S3 场景驱动
internal/order 订单聚合、状态机、领域事件
internal/inventory 库存预留、释放与不变量检查
internal/payment 支付状态、回调与退款
internal/ports 仓储、消息、时钟、协调接口
internal/adapters/ memory(L1/L2)、postgres/redis(L3)
internal/platform/ clock vclock simnet replication consensus
shard txn outbox idem reconcile idgen lease
scheduler cell
migrations/, testdata/ schema 与固定场景数据
tools/ deplint 依赖检查、invcheck 不变量检查
deploy/docker-compose.yml PostgreSQL 与 Redis(L3)
新增组件分成六组:
| 组 | 组件 | 解决的问题 |
|---|---|---|
| 时间与顺序 | clock、vclock、simnet | 时间不可比、并发事件无法排序、故障不可复现 |
| 复制与共识 | replication、consensus | 谁是权威、日志是否一致、切换丢多少 |
| 分区与路由 | shard、cell | 数据放哪、热点在哪、迁移与路由如何收敛 |
| 事务与消息 | txn、outbox | 跨库动作如何提交、消息如何不丢 |
| 幂等与对账 | idem、reconcile | 重复如何消除、差异如何发现与修复 |
| 协调与调度 | idgen、lease、scheduler | ID 唯一、锁安全、任务归属 |
依赖方向仍然是:internal/platform 不依赖具体业务;internal/order、internal/inventory、internal/payment 通过 ports 调用平台能力;adapters 实现端口。若某一节为了演示需要跨层引用,正文会说明原因,并在 tools/deplint 里加一条检查。
与卷二不同,卷三的 simnet 是一等组件。它不只是测试辅助,而是把“故障”变成可编排的输入:哪个节点宕机、哪些消息丢失、分区持续多久、恢复后保留哪些未提交日志。没有它,多节点实验只能靠真实环境,既慢又不可复现。
版本与环境
书中所有“实测”结论都基于下面这套环境。版本不同,结果可能不同,尤其是延迟和吞吐数字。
| 组件 | 版本 | 用途 |
|---|---|---|
| Go | 1.26.0 | 全书主线代码 |
| Java | Temurin 21.0.13 | 虚拟线程、并发容器与时间 API 对照 |
| 操作系统 | macOS darwin/arm64 | 本机测量环境 |
| Hugo | 0.163.3+extended | 书稿构建 |
几条口径:
- 卷三的大部分实验不依赖外部进程,使用内存适配器、确定性网络和
httptest; - 需要 PostgreSQL 或 Redis 的场景会明确标注 L3,并写明是否实际启动;
- 没有实际启动的依赖,正文写“未实测”或“预期行为”,不写“实测”;
- 分布式延迟、复制滞后、切换时间等数字,默认是目标值或示例值,不冒充多机实测;
- Java 结论必须用 JDK 21 真跑;没有跑过的代码,正文写“示例代码,未在本机运行”;
- 单次运行只代表当次环境,不包装成稳定结论。
本机默认 Java 可能不是 21,运行前先确认 java -version。如果版本不符,使用 Temurin 21 的本地解压包或容器,并在正文中写明实际使用的方式。
四级可运行标准
不是每段代码都值得启动数据库,也不是每个分布式场景都能在本机多机复现。书里把可运行性分成四级,读者可以按需选择验证深度。
L1:纯逻辑或单包
不依赖外部进程,只依赖标准库和项目内包。确定性网络模拟器、向量时钟、一致性哈希、Snowflake 位运算、租约状态机都属于这一级。
验收:
gofmt -l .
go vet ./...
go test ./...
并发代码再加 go test -race ./...。L1 能证明组件在给定输入下的行为,不能证明它在真实网络下的表现。
L2:进程内端到端
在一个进程里启动多个逻辑节点,用 simnet 传递消息,用 httptest 或 127.0.0.1:0 启动真实 HTTP 服务,适配器用内存实现,不连数据库。
验收:
go test -run TestScenarioS2 -v ./...
一个 L2 场景要完成创建、预留、支付三条断言,并检查超时、重复、崩溃和恢复后的状态。L2 能证明协议与业务编排在指定消息序列下成立,不能证明吞吐和真实网络抖动下的稳定性。
L3:真实依赖
使用 Docker Compose 启动 PostgreSQL 或 Redis,验证复制位点、事务隔离、锁过期、outbox 表和唯一约束等真实行为。
验收:
docker compose -f deploy/docker-compose.yml up -d
go test -tags=integration ./...
只承诺实际跑过的场景。没有跑过的数据库或缓存行为,写成“预期”或“需要验证”,不写成结论。卷三不要求每个读者都跑 L3,但书中出现 L3 结论时必须附环境与版本。
L4:多节点与故障注入
真实多机、真实网络分区、主备切换、跨机房复制、消息重复与乱序。L4 需要独立环境、网络控制和服务编排,成本远高于前三层。
卷三只在第 12 章容灾演练、第 13 章综合验证里讨论 L4 的设计与验收,不承诺完整的多机实验。出现 L4 场景时,会明确标注“需真实环境”,给出演练脚本和检查表,不伪造运行结果。
卷三承诺到哪一级
| 范围 | 承诺级别 | 说明 |
|---|---|---|
| 39 节正文 | 至少 L1 或 L2 | 每节都有可抽取、可编译的最小实现或场景 |
| 第 3、4、7、8、13 章 | L1 多节点模拟 | 复制、选主、事务、outbox、综合链路 |
| 第 5、6、9、10、11 章 | L1 为主,部分 L2 | 分片、检查器、对账、ID、锁、调度 |
| 第 12 章 | 设计 + 演练清单 | 多活与容灾不伪造多机结果 |
| 真实数据库/缓存 | 选择性 L3 | 只在关键语义处启动,正文标注 |
这个承诺的意思是:卷三能让你验证“逻辑是否正确、故障下不变量是否守住”,不能让你直接得到“生产能扛多少”的结论。后者需要卷二的方法加上真实环境。
代码标记与抽取
书里的 Go 代码块用三行注释标记来源:
// file: internal/platform/lease/lease.go
// tier: L1
// verify: go test ./internal/platform/lease/
package lease
三个字段的含义:
| 字段 | 含义 | 约束 |
|---|---|---|
file | 抽取后的相对路径 | 必须在 arch-lab 目录白名单内,不含 .. |
tier | 运行级别 | 只能是 L1/L2/L3/L4 |
verify | 验证命令 | 必须能在抽取后的模块里执行 |
抽取脚本会按文章分目录,把代码块写入独立模块,避免不同文章的包名和类型互相冲突。这样做的原因是:书稿不是一个大仓库,每篇文章的代码需要能单独编译,读者不必先理解全部上下文。
一个代码块只应包含一个完整文件。不要把同一个文件拆成“上半段/下半段”两个代码块,也不要用“(续)”之类的标记;抽取器会把它当成非法路径。如果确实需要多个文件,就给每个文件一个合法的 file 路径。
卷三新增一条约定:涉及多节点的代码,file 路径要能看出节点角色,例如 internal/platform/replication/leader.go 与 follower.go 分开;不要在一个文件里用全局变量模拟两个节点,否则测试之间会互相污染。节点实例应由 simnet 创建和销毁。
固定实验场景 S1/S2/S3
为了避免每篇文章都造一套数据,arch-lab 固定三组场景。它们覆盖卷三的三条主线:复制与切换、跨分片事务、重复与对账。
| 场景 | 拓扑 | 关键动作 | 验证目标 |
|---|---|---|---|
| S1 单主复制与切换 | 1 主 2 从,确定性网络 | 写入、复制、杀主、选主、旧主恢复 | 已提交日志不丢、同任期不双主、状态不回退 |
| S2 跨分片下单 | 订单按用户分片、库存按 SKU 分片 | 创建订单、预留库存、支付、履约 | 不超卖、状态单向、支付只生效一次 |
| S3 重复与对账 | 单分片 + 重复回调 + 对账任务 | 重复投递、消费崩溃、T+1/实时对账 | 副作用只发生一次、差异可分类可收敛 |
场景里的客户、商品、金额都是示例值,不是生产数据。正文出现具体数字时优先复用这三组,避免读者在多个示例之间来回对照。
S1:单主复制与切换
拓扑是一个 leader、两个 follower,消息经过 simnet,可以按规则延迟、丢弃或分区。初始数据沿用示例客户 cus-001 和商品 sku-book,订单金额 4999 分。实验步骤:
- 客户端向 leader 写入一条订单事件,leader 追加日志并复制;
- 多数派确认后提交,向客户端返回提交位点;
- 注入 leader 崩溃,剩余节点在随机超时后发起选举;
- 新 leader 产生后继续接受写入,旧 leader 恢复;
- 旧 leader 携带未提交尾部重新加入,必须截断并追平;
- 客户端用会话令牌读取,确认自己的写可见。
要检查的不变量:
- 已向客户端确认的提交,在后续所有 leader 的日志中都存在;
- 同一任期内最多一个 leader;
- 每个 follower 的
lastApplied单调不减; - 旧 leader 恢复后,在追平前不接受写,也不以旧任期响应读。
正文会分别讨论同步复制与异步复制的差别:同步模式下可以把“已确认写不丢”作为目标,代价是可用性和延迟;异步模式下必须承认一个非零的丢失窗口,并把它换算成 RPO。两种模式都不能靠“切换脚本写得仔细”消除窗口。
S2:跨分片下单
订单库按 customer_id 分片,库存库按 sku_id 分片,订单与库存在不同分片、不同存储。下单链路是:创建订单 → 预留库存 → 创建支付单 → 支付回调 → 履约。
实验使用示例客户 cus-003、商品 sku-book 两件、单价 4999 分,总价 9998 分。下单步骤:
- 客户端提交下单请求,携带幂等键;
- 订单分片写入
CREATED与 outbox 记录; - relay 投递预留命令,库存分片执行条件扣减;
- 库存成功后写回预留结果,订单进入
INVENTORY_RESERVED; - 支付成功后订单进入
PAID,履约后进入FULFILLED; - 任一步超时或失败,按 Saga 反向补偿,或留给对账收敛。
注入的故障包括:重复提交同一幂等键、库存提交后响应丢失、relay 在投递后崩溃、消费端在副作用后确认前崩溃、支付回调重复。要检查的不变量:
- 库存
available不为负,available + reserved等于初始总量; - 订单状态只沿主链推进,回退必须有冲正记录;
- 同一订单最多一条有效支付;
- 同一幂等键的重复请求返回同一订单号,不产生第二张订单;
- 补偿完成后,没有悬挂的库存预留。
S2 是卷三最完整的链路实验。它同时用到分片路由、Outbox、消费幂等、Saga 和对账,适合在读完第 7–9 章后整体跑一遍。
S3:重复与对账
支付渠道对同一笔支付回调投递三次,事件 ID 相同;消费端第一次处理成功但在确认前崩溃,第二次重复处理;对账任务分别以 T+1 全量和实时流式两种方式运行。
示例数据沿用 cus-001、sku-book 一件、4999 分。要检查的不变量:
- 支付副作用只发生一次,重复回调返回同一结果;
- 订单已支付金额等于支付流水成功金额之和;
- 对账差异能按漏单、重复、金额、状态分类;
- 自动修复后差异数下降,剩余差异有明确责任人和处置状态。
S3 的重点不是“消息中间件保证恰好一次”,而是承认投递通常是至少一次,把“恰好一次”拆成“至少一次投递 + 消费端去重 + 对账兜底”。这也是卷三对“恰好一次”的一贯口径。
数字口径
书里会出现三类数字,读的时候要分清:
| 类型 | 特征 | 例子 |
|---|---|---|
| 实测值 | 附命令、环境、样本量、运行次数 | 本机单进程模拟器中某次测试的计数 |
| 目标值 | 设计目标、SLO 或验收门槛 | 同步复制切换的 RPO 目标为 0 |
| 示例值 | 用于说明公式、配置形状或场景数据 | 库存 1 件、订单金额 4999 分 |
卷三的多数分布式场景没有真实多机环境,因此不会出现“多节点实测吞吐”“跨机房实测延迟”这类数字。涉及延迟、复制滞后、切换时间的讨论,要么是目标值,要么是示例值,要么是单进程模拟器里的逻辑计数。
单进程模拟器能验证协议逻辑和不变量,不能证明生产性能。即使某个数字来自实际运行,也只代表当次环境、当次数据规模和当次故障编排,不能外推成容量承诺。
区分方法很简单:看它有没有附带命令、环境、样本量和运行次数。四样缺一件,就不应当作实测值使用。
两条阅读路线
同一本书,不同角色的读法不同。下面给两条路线,避免从第一页按顺序读到最后一页却抓不住重点。
路线一:顺序读
适合第一次接触分布式系统,或者想把语言体系补齐的读者。
| 阶段 | 章节 | 目标 |
|---|---|---|
| 建立语言 | 第 1–2 章 | 知道部分失败、故障模型、时钟与顺序为什么重要 |
| 理解复制 | 第 3–4 章 | 能解释复制位点、选主、提交规则和切换窗口 |
| 理解分区 | 第 5–6 章 | 能选分片键、识别热点、把一致性模型落到不变量 |
| 处理事务 | 第 7–9 章 | 能设计跨库链路、消息投递、幂等与对账 |
| 做协调与容灾 | 第 10–12 章 | 能设计 ID、锁、调度、多活与切换演练 |
| 综合验证 | 第 13 章 | 把前面的组件放进一个订单系统跑一遍 |
顺序读的关键不是记住每个协议,而是每读完一段,能回答“这段解决了哪一个未证明假设”。第 1 章列出的问题,会在后面章节逐个回收。
路线二:带着一次跨库改造来
适合手里已经有一个单库订单系统,正准备分片、拆服务或接入消息的读者。不要从第 1 章开始,直接按改造步骤跳转:
- 先读 6.3,把“库存不超卖、订单状态单向、支付不重复”写成可检查的不变量;
- 读 1.1 和 1.2,确认改造后会出现哪些新的失败模式;
- 读 5.1 和 5.2,选择分片键,设计迁移与回滚;
- 读 7.2 和 7.3,确定跨库步骤用 Saga 还是 TCC,哪些步骤不可补偿;
- 读 8.1 和 8.2,把业务写入与消息投递放进同一个本地事务,消费端做去重;
- 读 9.1 和 9.2,补幂等键、对账与差异处置;
- 读 10.1 和 10.2,检查全局 ID 与锁在切换、回拨下是否安全;
- 最后读 13.1 和 13.2,按综合实验的检查表做一次迁移演练。
这条路线不要求先读复制和共识。等改造跑通、遇到“主库切换后读不到刚写的订单”或“分区期间两个节点都能写”时,再回读第 3–4 章,问题会更具体。
每篇文章的结构
卷三 39 篇正文的写法基本一致,读的时候可以先看骨架,再决定细读哪一段。
| 部分 | 作用 | 读法 |
|---|---|---|
| 开头的场景 | 用一个具体问题把本节目标说清楚 | 判断是否与你的问题相关 |
| 机制与取舍 | 解释协议、模型或组件的代价 | 重点读,容易被跳过 |
| 代码与测试 | 可抽取、可运行的最小实现 | 至少跑 L1 |
| arch-lab 的落点 | 这个机制放在哪一层、依赖谁 | 对照卷一的结构 |
| 故障注入 | 注入什么、观察什么、判定什么 | 卷三新增,重点看 |
| 常见错误与边界 | 五条左右的失败模式 | 上线前逐条自查 |
| 小结 | 可带走的结论与下一节入口 | 决定继续读还是回查 |
不是每篇都能跑 L3 或 L4,但每篇至少有一个 L1 或 L2 片段。如果某篇的代码在你的环境里跑不通,先检查 Go 版本、包路径和代码块是否被截断,再对照正文里的完整实现;不要只改测试让它通过,那会把书里的结论和你的代码一起改坏。
每篇结尾的“常见错误与边界”不是凑数。它列的通常是同一类机制在真实系统里失效的方式,比如租约锁的时钟回拨、Raft 的旧任期响应、Outbox 的重复投递、对账的差异分类错误。读代码只能看到“正确路径”,这一节补的是“错误路径”。
怎么验证一篇的代码
以 10.2 的租约锁为例,最小验证流程是:
- 从代码块中复制
// file:标记和代码,放到临时模块的对应路径; - 在模块根目录执行
go mod init archlab,写入go 1.26.0; - 执行
gofmt -l .、go vet ./...、go test ./...; - 并发相关测试追加
go test -race ./...; - 如果用了
simnet,先跑单节点测试,再跑双节点分区测试; - 如果失败,先检查 Go 版本和包路径,再检查代码块是否被截断。
书里的代码块都按这个流程设计,但抽取过程可能有复制误差。发现跑不通时,先对照文章中的完整代码块,而不是直接怀疑结论。
涉及 L3 的代码,还需要启动 deploy/docker-compose.yml,并用 -tags=integration 运行。正文会写明数据库版本、初始化脚本和清理方式;没有写明的,不要假设它已经被验证。
与站内文章的关系
站内 distributed-systems/ 与 architecture/ 两个专题已经覆盖了 CAP、复制、一致性哈希、分布式事务、幂等、限流等题目。卷三不重复这些文章的定义和清单,只在必要处用一段话概括,并用站内链接指过去。
卷三新增的是:
- arch-lab 的可运行多节点模拟器与故障编排;
- 每条协议或机制对应的不变量检查;
- 跨库下单、切换、对账三组固定场景;
- Go 主线与 Java 21 的结构对照;
- 把“最终一致”写成可验收条件的检查表。
如果你已经读过那些文章,卷三可以当作“把它们放进同一个订单系统,真正跑一遍”的实践册;如果你没读过,也不影响阅读,正文会在第一次出现概念时给出最小定义和延伸阅读入口。
小结
卷三在卷一、卷二的 arch-lab 上增加六组能力:时间与顺序、复制与共识、分区与路由、事务与消息、幂等与对账、协调与调度。13 章分成四段,从分布式语言走到综合实验。代码按 L1–L4 分级,L1 是默认,L2 用于进程内端到端,L3 用于真实依赖,L4 只给设计与演练清单。
记住四条使用约定:
- 代码块带
file/tier/verify标记,每篇代码应能独立编译; - 多节点实验优先用
simnet,不要用全局变量假装多个节点; - 数字要分清实测值、目标值和示例值,多机性能不伪造;
- 每读完一节,回到不变量:这条机制到底保证了哪条业务规则。
剩下的,从 1.1 开始。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。