《后端架构设计·分布式与数据一致性》前言

说明卷三怎么用:13 章四段结构、arch-lab 新增的时钟/复制/共识/分片/事务/对账组件、L1–L4 可运行性分级、代码块 file/tier/verify 标记、S1–S3 固定场景、实测与目标与示例的数字口径,以及顺序阅读和带着一次跨库改造来读的两条路线。

这篇前言解决什么问题

序讲的是“为什么写”,前言讲的是“怎么用”。具体包括:

  • 卷三在卷二之后接什么,哪些问题留给卷四;
  • 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、schedulerID 唯一、锁安全、任务归属

依赖方向仍然是:internal/platform 不依赖具体业务;internal/order、internal/inventory、internal/payment 通过 ports 调用平台能力;adapters 实现端口。若某一节为了演示需要跨层引用,正文会说明原因,并在 tools/deplint 里加一条检查。

与卷二不同,卷三的 simnet 是一等组件。它不只是测试辅助,而是把“故障”变成可编排的输入:哪个节点宕机、哪些消息丢失、分区持续多久、恢复后保留哪些未提交日志。没有它,多节点实验只能靠真实环境,既慢又不可复现。

版本与环境

书中所有“实测”结论都基于下面这套环境。版本不同,结果可能不同,尤其是延迟和吞吐数字。

组件版本用途
Go1.26.0全书主线代码
JavaTemurin 21.0.13虚拟线程、并发容器与时间 API 对照
操作系统macOS darwin/arm64本机测量环境
Hugo0.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 分。实验步骤:

  1. 客户端向 leader 写入一条订单事件,leader 追加日志并复制;
  2. 多数派确认后提交,向客户端返回提交位点;
  3. 注入 leader 崩溃,剩余节点在随机超时后发起选举;
  4. 新 leader 产生后继续接受写入,旧 leader 恢复;
  5. 旧 leader 携带未提交尾部重新加入,必须截断并追平;
  6. 客户端用会话令牌读取,确认自己的写可见。

要检查的不变量:

  • 已向客户端确认的提交,在后续所有 leader 的日志中都存在;
  • 同一任期内最多一个 leader;
  • 每个 follower 的 lastApplied 单调不减;
  • 旧 leader 恢复后,在追平前不接受写,也不以旧任期响应读。

正文会分别讨论同步复制与异步复制的差别:同步模式下可以把“已确认写不丢”作为目标,代价是可用性和延迟;异步模式下必须承认一个非零的丢失窗口,并把它换算成 RPO。两种模式都不能靠“切换脚本写得仔细”消除窗口。

S2:跨分片下单

订单库按 customer_id 分片,库存库按 sku_id 分片,订单与库存在不同分片、不同存储。下单链路是:创建订单 → 预留库存 → 创建支付单 → 支付回调 → 履约。

实验使用示例客户 cus-003、商品 sku-book 两件、单价 4999 分,总价 9998 分。下单步骤:

  1. 客户端提交下单请求,携带幂等键;
  2. 订单分片写入 CREATED 与 outbox 记录;
  3. relay 投递预留命令,库存分片执行条件扣减;
  4. 库存成功后写回预留结果,订单进入 INVENTORY_RESERVED;
  5. 支付成功后订单进入 PAID,履约后进入 FULFILLED;
  6. 任一步超时或失败,按 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 章开始,直接按改造步骤跳转:

  1. 先读 6.3,把“库存不超卖、订单状态单向、支付不重复”写成可检查的不变量;
  2. 读 1.1 和 1.2,确认改造后会出现哪些新的失败模式;
  3. 读 5.1 和 5.2,选择分片键,设计迁移与回滚;
  4. 读 7.2 和 7.3,确定跨库步骤用 Saga 还是 TCC,哪些步骤不可补偿;
  5. 读 8.1 和 8.2,把业务写入与消息投递放进同一个本地事务,消费端做去重;
  6. 读 9.1 和 9.2,补幂等键、对账与差异处置;
  7. 读 10.1 和 10.2,检查全局 ID 与锁在切换、回拨下是否安全;
  8. 最后读 13.1 和 13.2,按综合实验的检查表做一次迁移演练。

这条路线不要求先读复制和共识。等改造跑通、遇到“主库切换后读不到刚写的订单”或“分区期间两个节点都能写”时,再回读第 3–4 章,问题会更具体。

每篇文章的结构

卷三 39 篇正文的写法基本一致,读的时候可以先看骨架,再决定细读哪一段。

部分作用读法
开头的场景用一个具体问题把本节目标说清楚判断是否与你的问题相关
机制与取舍解释协议、模型或组件的代价重点读,容易被跳过
代码与测试可抽取、可运行的最小实现至少跑 L1
arch-lab 的落点这个机制放在哪一层、依赖谁对照卷一的结构
故障注入注入什么、观察什么、判定什么卷三新增,重点看
常见错误与边界五条左右的失败模式上线前逐条自查
小结可带走的结论与下一节入口决定继续读还是回查

不是每篇都能跑 L3 或 L4,但每篇至少有一个 L1 或 L2 片段。如果某篇的代码在你的环境里跑不通,先检查 Go 版本、包路径和代码块是否被截断,再对照正文里的完整实现;不要只改测试让它通过,那会把书里的结论和你的代码一起改坏。

每篇结尾的“常见错误与边界”不是凑数。它列的通常是同一类机制在真实系统里失效的方式,比如租约锁的时钟回拨、Raft 的旧任期响应、Outbox 的重复投递、对账的差异分类错误。读代码只能看到“正确路径”,这一节补的是“错误路径”。

怎么验证一篇的代码

以 10.2 的租约锁为例,最小验证流程是:

  1. 从代码块中复制 // file: 标记和代码,放到临时模块的对应路径;
  2. 在模块根目录执行 go mod init archlab,写入 go 1.26.0;
  3. 执行 gofmt -l .、go vet ./...、go test ./...;
  4. 并发相关测试追加 go test -race ./...;
  5. 如果用了 simnet,先跑单节点测试,再跑双节点分区测试;
  6. 如果失败,先检查 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 开始。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《互联网产品的 AI 架构·平台化与治理》附录 C 平台化评审清单
  2. 《互联网产品的 AI 架构·平台化与治理》附录 B 能力目录与配额字段速查
  3. 《互联网产品的 AI 架构·平台化与治理》附录 A 治理角色与职责速查