三个熟悉的场景
先说三个场景。它们由多个项目里的常见问题拼合而成,不对应任何一家公司,也不指向任何具体个人。三个场景分别发生在下单、库存和故障切换三个时刻,表面上属于不同模块,根因却是同一个:系统已经跨过了单机边界,团队仍在用单机直觉做判断。
场景一:跨库下单只成功了一半
订单服务写完订单表,调用库存服务预留库存;库存服务返回成功,订单服务在写“已预留”状态时超时。客户端收到超时,用户点了一次重试;重试请求创建了第二张订单,第一张订单的库存也已经扣减。两张订单最终都进入待支付,库存只够发一单。
值班同学查日志,看到的是三段各自成功的记录:订单插入成功、库存扣减成功、支付单创建成功。没有一条日志记录“这三次成功属于同一次业务动作”,也没有一个组件负责在超时后判断第一张订单到底该继续还是取消。所谓“分布式事务”,在事故现场退化成了几个人拉群对数据。
这个场景的关键不是“重试没做幂等”,而是:一次业务动作被拆到多个存储之后,“成功”不再是布尔值。它至少包含四种状态:确定成功、确定失败、结果未知、部分成功。系统如果没有为“结果未知”设计查询与收敛路径,就只能靠人肉对账。
场景二:库存扣成了负数
库存表有 available 字段,扣减逻辑是“先查再改”:事务里读出可用数量,应用层判断是否足够,再执行扣减。单库单实例时,这段代码在压测里从没出过错。
上了两个实例之后,同一 SKU 的两次请求同时读到 available = 1,都判断可以扣,都执行了扣减。数据库的行锁只保证两条 UPDATE 串行,不保证“读到的值在更新时仍然有效”。最终库存变成 -1,超卖了 1 件。
更麻烦的是,事后没有一条不变量被持续检查。库存表允许负数,订单表允许“已支付但无库存”,两端各自看都“有记录”,只有把两张表放在一起算,才能发现总数对不上。团队的第一反应是给扣减加分布式锁,第二反应是锁超时后继续超卖。
这个场景说明:并发正确性不是“加个锁”就能解决的。锁的粒度、持有时间、失效后的行为,以及数据库是否提供条件更新,共同决定不变量能否守住。卷一在聚合一致性边界 里讨论过“一个事务只改一个聚合”,卷三要补的是:当不变量跨聚合、跨分片时,谁来保证它。
场景三:主库切换后订单“倒退”
主库故障,运维把备库提升为新主。健康检查通过,流量切过去,支付回调开始写入。十分钟后,旧主恢复,复制关系没有正确重建,旧主仍然接受了一部分写请求。同一张订单在两个节点上分别被改成已支付和已取消。
用户看到的状态在“已支付”和“已取消”之间来回跳;客服系统读到取消,财务系统读到支付;对账任务把两边的差异记成“渠道差错”,实际是数据库层面的双写。事后复盘写的是“切换脚本需要加固”,没有人回答:新主的位点是否包含了旧主已确认的写,旧主恢复后凭什么还能接受写,谁有资格判断哪条状态是最终状态。
这个场景的关键不是“切换失败”,而是:故障切换改变了“谁是权威”的定义,而系统里没有任何机制把权威变更传达给所有写入方。健康检查只能证明进程活着,不能证明数据完整;主备角色只是一个配置,不是一条被数据层强制执行的规则。卷二讨论过主备切换与接管条件 ,但那里的边界是单库主备;卷三要把复制位点、任期和写拒绝一起纳入切换条件。
这三个场景的共同点
三个场景的模块不同、触发条件不同,但都可以归结为同一句话:系统已经分布式,判断仍然单机化。
单机时代,一次函数调用要么返回,要么抛异常;一个事务要么提交,要么回滚;一台机器上的时钟只有一个;一次写入只有一个权威副本。跨过进程和网络边界之后,这四个前提全部失效:
- 调用可能“没有结果”,而不是成功或失败;
- 事务边界跨不过网络,本地提交不等于全局提交;
- 每个节点有自己的时钟,时间戳不能直接比较;
- 同一个业务对象可能同时存在多个可写副本。
卷一回答了边界在哪里,卷二回答了边界之内的代价是多少。到了跨节点这一步,问题变成了:当边界本身会断开、会延迟、会重复、会失去共识时,业务不变量靠什么守住。这就是卷三要处理的对象。
卷二交给卷三的六个未证明假设
卷二把单机内的性能与故障治理讲到了可以测量的程度。但它的很多结论建立在一组没有证明的假设上。这些假设在单进程内成立,一旦跨节点就不再成立。卷三的每一章,几乎都在处理其中一个假设的失效。
| 编号 | 卷二里的隐含假设 | 跨节点后的现实 | 卷三的落点 |
|---|---|---|---|
| H1 | 单机测出的容量可以按比例外推 | 协调、复制和网络往返引入新的固定成本 | 第 3、5、13 章 |
| H2 | 进程内注入的超时等价于网络故障 | 真实故障包含丢包、分区、重复与乱序 | 第 1、4 章 |
| H3 | 所有节点共享同一个时钟 | 时钟会漂移、回拨,先后关系不再显然 | 第 2、10 章 |
| H4 | 一次调用只有成功和失败 | 存在“结果未知”,调用方必须能查询和收敛 | 第 7、8 章 |
| H5 | 幂等只在单库单连接内成立 | 跨服务、跨分片后,副作用与确认可能分离 | 第 8、9 章 |
| H6 | 观测到的数据就是全部数据 | 复制延迟、丢消息和未提交状态让视图不完整 | 第 3、6、9 章 |
假设一:单机结论可以外推
卷二用压测得到的 QPS、P99 和实例数,前提是请求只经过本进程和本地依赖。跨节点之后,每一次远程调用都增加一次不可忽略的排队与网络开销,重试会放大流量,复制会占用带宽,协调会引入额外往返。把单机容量乘以节点数,得到的往往是一个偏乐观的上界。
卷三不会推翻卷二的测量方法,而是要求在测量里加入新的成本项:跨节点往返、复制确认、分区期间的降级、切换时的不可用窗口。看不到这些成本,扩容就只是把瓶颈从 CPU 挪到网络和协调层。
假设二:进程内故障注入等价于真实故障
卷二用可控延迟、可控错误和可控实例摘除做故障注入,这些手段可复现,适合验证代码路径。但真实网络还会丢包、乱序、重复、分区,并且这些现象经常同时发生。进程内“关闭一个适配器”与“两个节点互相看不见但都能接受写入”不是一回事。
卷三因此把故障模型单独拿出来讲,并用确定性网络模拟器构造可复现的分区、丢包和重复。模拟器不是真实网络的替代品,它的价值在于把某一种故障语义固定下来,让不变量检查可以反复执行。
假设三:所有节点共享同一个时钟
卷二里“超时 200ms”“缓存 TTL 30 秒”这样的表述,默认时间是可比的。跨节点之后,两个节点的墙上时钟可能相差几十毫秒,NTP 校正还可能让时间回拨。用本地时间给事件排序,会把并发事件误判成先后关系,也会让“最后写入获胜”丢掉本应保留的更新。
卷三从物理时钟讲到逻辑时钟、向量时钟和混合逻辑时钟,目的不是让读者实现一个时钟库,而是知道在什么场景下不能依赖时间戳,以及不能依赖时应该换成什么。
假设四:一次远程调用只有成功和失败
本地函数调用不会“成功但对方没收到”,远程调用会。请求发出后连接断开,调用方无法区分三种情况:对方没处理、对方处理了但响应丢了、对方还在处理。把超时一律当作失败并重试,就可能制造重复副作用;一律当作成功,就可能丢失业务动作。
卷三把这种“结果未知”当作一等状态,要求接口提供查询能力,要求业务动作具备幂等键,要求补偿或对账能够收敛。这不是某一个框架的功能,而是接口设计的一部分。
假设五:幂等只在单库内成立
卷二讨论过去重表和幂等键 ,场景大多是一个服务、一个数据库。跨服务之后,业务写入与消息发送不在同一个本地事务里:数据库提交了,消息可能没发出去;消息发出去了,消费端可能重复收到;消费端处理了,确认可能丢失。每一个断点都会产生新的不一致窗口。
卷三用事务性 Outbox、消费端去重、幂等键作用域和对账系统,把这些断点逐个补上。重点不是追求一个抽象的“恰好一次”,而是明确每个环节的投递语义,以及重复发生时由谁负责消除副作用。
假设六:观测到的数据就是全部数据
卷二假设监控、日志和 trace 能还原一次请求的完整路径。分布式环境里,副本可能滞后,消息可能还在队列里,事务可能处于中间状态。你在界面上看到“订单未支付”,不代表支付没有发生,只代表当前读到的副本还没有应用那条记录。
卷三要求每个一致性结论都附带读的来源:读的是主库还是副本,位点是多少,是否经过会话保证,是否存在未消费的消息。没有这些上下文,“现在是什么状态”这句话没有意义。
本书的四个立场
立场一:部分失败是常态,不是异常
分布式系统里,节点会挂、网络会断、磁盘会满、依赖会变慢。把部分失败当作“不应该发生”的异常,系统就会在第一次真实故障时暴露出一连串未定义行为:重试风暴、状态卡死、双写覆盖、对账缺口。
卷三把部分失败当作设计输入。每一个跨节点动作都要回答:如果只完成了一半,系统处于什么状态,谁来发现,谁来继续,谁来撤销。没有这三个答案,所谓“高可用”只是把故障推迟到下一次。
立场二:顺序不是免费的
很多人默认消息按发送顺序到达、事件按发生顺序被处理、数据库按提交顺序复制。实际上,顺序需要成本:单分区、单主库、串行确认、等待复制,都会限制吞吐和可用性。跨分区之后,全局顺序几乎不可能免费获得。
卷三会反复问:这个业务真的需要全局顺序,还是只需要某个聚合内的顺序?订单状态机需要单订单有序,库存扣减需要单 SKU 有序,不同订单之间通常不需要。把顺序范围缩小,才能把成本降下来;把顺序范围说清楚,才能判断消息中间件和数据库的承诺是否够用。
立场三:一致性是业务不变量,不是技术口号
“强一致”“最终一致”“线性一致”这些词,如果不落到具体不变量上,就无法验收。库存不超卖是一条不变量,订单状态只能单向推进是一条不变量,同一笔支付不能重复扣款也是一条不变量。每条不变量都需要说明:在哪个存储上检查、允许短暂违反多久、违反后如何修复。
卷三不会把“用了 Raft”或“用了分布式事务”当作一致性已经解决的证据。它会要求把不变量写成可执行的检查,再反推需要多强的一致性模型。技术选型服务于不变量,而不是反过来。
立场四:可验证胜过可描述
架构图可以画得很完整,方案文档可以写得很漂亮,但只有测试和故障注入能回答“系统在异常时到底会怎样”。卷三优先选择可以运行、可以注入、可以重复的实验:进程内多节点模拟、确定性消息传递、历史记录检查器、对账收敛测试。
不能运行的部分,卷三会明确标注为设计推演或目标值,不包装成实测结论。一个诚实的“未验证”,比一个看起来精确的假数字更有价值。
一次跨库下单需要回答的五个问题
把场景一抽象出来,一次跨库下单至少要回答五个问题。这五个问题贯穿卷三的多个章节,也是读这本书时可以反复使用的检查表。
问题一:业务动作的边界在哪里
订单创建、库存预留、支付单创建,哪些属于同一个业务动作,哪些可以独立失败?边界划得太大,协调成本高、失败范围广;划得太小,中间状态多、补偿链长。卷一用聚合和一致性边界讨论过这个问题,卷三则要把它放到跨库环境下重新检查。
问题二:每一步的结果是否可查询
调用超时后,调用方能不能根据一个业务键查到对方是否处理成功?如果接口只提供“执行”不提供“查询”,调用方就只能猜。可查询性是幂等和对账的前提,也是把“结果未知”变成可收敛状态的前提。
问题三:重复执行会不会产生第二个副作用
重试、消息重投、用户重复点击都会导致同一业务动作被执行多次。系统需要知道用什么键去重、键的作用域多大、保留多久、并发同键时谁赢。没有这些定义,“幂等”只是一个愿望。
问题四:失败后是继续、补偿还是人工介入
不同步骤的失败处理不同。有的可以重试,有的必须补偿,有的不可补偿只能人工处理。卷三要求把每一步的失败策略写出来,并且明确补偿失败后由谁接手。Saga 和 TCC 不是万能模板,它们只是把这些问题显式化的工具。
问题五:最终由谁判断数据已经一致
即使每一步都做了幂等和补偿,仍然可能有漏网之鱼。对账系统是最后一道防线:它按业务键比对两边的记录,分类差异,自动修复可修复的部分,把剩余差异交给人工。没有对账,所谓“最终一致”没有验收标准。
三个常见反模式
反模式一:把中间件当答案
“上个分布式事务框架就好了”“换成支持强一致的数据库就好了”“加个分布式锁就好了”。这些说法共同的问题是:没有说明不变量是什么、故障时中间件自己会怎样、中间件失效后业务如何降级。
中间件能降低实现成本,但不能替业务定义正确性。卷三会拆开这些组件的能力边界,例如 Redis 锁在主从切换下的安全窗口、2PC 在协调者故障时的阻塞、Outbox 在 relay 崩溃后的重复投递。知道边界,才知道什么时候不能只靠它。
反模式二:用重试掩盖不确定性
重试是处理瞬时故障的有效手段,但它不解决“对方是否已经执行”的问题。对非幂等操作重试,会把一次失败放大成多次副作用;对已经过载的下游重试,会进一步加剧拥塞。
卷三要求重试必须和幂等键、重试预算、退避抖动一起设计。没有这三样,重试只是把故障从调用方转移到被调用方。
反模式三:把“最终一致”当成“不用管”
最终一致不是“现在不一致没关系”,而是“存在一个可证明会到达一致的机制”。这个机制必须说明:靠什么传播更新、冲突如何解决、延迟上界是多少、传播失败如何发现、发现后如何修复。
如果答不出这些,“最终一致”只是“我们也不知道什么时候会一致”的委婉说法。
为什么仍然以 Go 为主线
卷三继续使用 Go 1.26.0 和订单系统 arch-lab。原因不是 Go 擅长分布式,而是它的运行时足够小,能把注意力放在协议和状态机上。goroutine、channel、context 和标准库网络能力,足以实现一个可读的复制日志、选主流程和确定性消息总线,而不需要先理解一套框架的抽象。
Go 的显式错误处理也适合这本书的主题。远程调用返回错误时,代码必须决定是重试、补偿还是向上返回;这种“不能假装没发生”的风格,正好对应分布式系统里“不能假装成功”的要求。
卷三的 Go 代码以 L1 和 L2 为主:进程内多节点模拟、确定性网络、内存存储、HTTP 端到端。少数场景使用真实 PostgreSQL 或 Redis,标注为 L3;真实多机、真实网络分区标注为 L4,只给设计推演和演练清单,不伪造运行结果。
为什么保留 Java 21 对照
Java 21 在企业后端仍然有大量存量系统,完全回避它会让书脱离读者的工作环境。卷三的对照集中在结构差异,而不是性能排名:
- 虚拟线程与 goroutine 的调度和阻塞语义不同,影响“一个连接一个线程”的迁移判断;
CompletableFuture、ExecutorService与 goroutine、channel、context 的组合方式不同;ReentrantLock、StampedLock与 Go 的互斥锁在可重入、读写分离和条件等待上语义不同;Clock、Instant与 Go 的单调时钟在时间来源和比较方式上不同;- 分布式 ID、租约、重试的库生态不同,但需要回答的问题相同。
Java 代码如果没有在本机 JDK 21 上运行,正文会明确写“示例代码,未在本机运行”。跨语言对照的目的不是证明谁更好,而是让读者在两种生态之间迁移时,知道哪些语义可以照搬,哪些必须重写。
卷三不承诺什么
- 不实现生产级 Raft:书里的迷你 Raft 用于讲清角色、任期、日志匹配和提交规则,不包含工程级的网络、存储、快照和运维细节;
- 不伪造多节点实测:没有真实多机环境的地方,只写目标值、示例值和预期行为;
- 不替代中间件文档:Raft、Kafka、Redis、数据库复制组的参数和运维手册不在本书范围;
- 不承诺某一套拓扑适合所有业务:单元化、双活、多活各有代价,书里给判断方法,不给万能答案;
- 不覆盖组织治理:团队边界、预算、平台组织方式留给卷四;
- 不把“跑通示例”等同于“生产可用”:示例验证的是逻辑和不变量,不是容量和运维。
四卷本里的位置
《后端架构设计》共四卷:
- 卷一《架构基础与边界》:边界、决策、契约、数据归属和最小治理闭环;
- 卷二《高性能与高可用》:测量、容量、缓存、异步、隔离、限流、超时重试、故障域、发布与观测;
- 卷三《分布式与数据一致性》:复制、共识、分区、一致性模型、分布式事务、可靠消息、幂等对账、协调与多活;
- 卷四《演进与治理》:微服务拆分、平台工程、SLO、混沌、发布工程与成本治理。
卷三处在“单体内功”与“组织治理”之间。它假设读者已经有一个能运行、能测量、能处理单机故障的系统;如果没有,卷三的分布式组件只会把未解决的问题复制到更多节点上。
卷二最后一节已经把未证明的假设列给了卷三,卷三的最后一节会把仍未证明的假设列给卷四。四卷不是四个孤立的话题,而是一条逐步扩大证据边界的路径。
这本卷三适合谁
适合:
- 已经能写业务代码,但说不清跨库操作失败后系统处于什么状态的工程师;
- 正在做分库分表、主备切换、消息可靠性或对账体系建设的后端负责人;
- 读过卷一、卷二,想把“单机正确性”推进到“跨节点正确性”的读者;
- 需要一套可运行示例和检查清单,给团队做分布式一致性培训的人;
- 正在评审分布式方案,想识别“用了中间件但没定义不变量”这类问题的架构师。
不适合:
- 想找某个中间件参数调优手册的读者;
- 想找“千万级 QPS 多活架构”模板的读者;
- 只想看结论、不愿意跑代码或做故障注入的读者;
- 期待完整生产级 Raft、分布式数据库内核实现的读者;
- 希望本书替团队做组织决策的读者。
关于数字的承诺
卷三会出现三类数字,读的时候要分清:
| 类型 | 特征 | 例子 |
|---|---|---|
| 实测值 | 附命令、环境、样本量、运行次数 | 本机单进程模拟器中某次测试的计数 |
| 目标值 | 设计目标、SLO 或验收门槛 | 切换后 RPO 目标为 0 |
| 示例值 | 用于说明公式、配置形状或场景数据 | 库存 1 件、订单金额 4999 分 |
卷三的多数分布式场景没有真实多机环境,因此正文不会出现“多节点实测吞吐”这类数字。涉及延迟、复制滞后、切换时间的讨论,要么是目标值,要么是示例值,要么是单进程模拟器里的逻辑计数。单进程模拟器能验证协议逻辑和不变量,不能证明生产性能。
目标值和示例值不是实测值,不能拿来证明系统能力;即使是实测值,也只代表当次环境。卷三会尽量把“这是假设”“这是目标”“这是某次运行的观察”分开写,避免读者把示例当成承诺。
怎么判断一条一致性结论是否可信
读到“系统保证强一致”“切换不丢数据”“消息恰好一次”这类结论时,可以用下面这张表检查。
| 检查项 | 可信的表述 | 不可信的表述 |
|---|---|---|
| 不变量 | 明确写出哪条业务规则不被违反 | 只说“保证一致性” |
| 范围 | 单订单、单 SKU、单分片还是全局 | 不说明作用域 |
| 故障模型 | 崩溃、丢包、分区、拜占庭中的哪一种 | 不说明假设的故障 |
| 读来源 | 主库、副本、位点、会话令牌 | 不说明读的是哪个副本 |
| 失败窗口 | 允许短暂不一致的时长与上界 | 只说“最终会一致” |
| 检测 | 用什么指标或对账发现违反 | 没有检测手段 |
| 恢复 | 违反后由谁、按什么步骤修复 | 只写“人工处理” |
| 证据 | 测试、注入、历史记录检查 | 只有架构图和口头承诺 |
这张表也适用于本书。如果某一节只给结论不给验证方式,读者应该保留怀疑;如果给了可运行的检查,至少可以复现或证伪。
一次切换失败的分析示例
以场景三为例,走一遍卷三的分析方式。
第一步:定义不变量。同一订单在同一时刻只能有一个权威状态,状态只能沿 CREATED → INVENTORY_RESERVED → PAID → FULFILLED 推进,任何回退都需要显式的冲正记录。
第二步:确定权威。正常情况下主库是唯一权威。切换不只是把流量指向备库,还要确保旧主不再接受写,并且新主的日志包含所有已向客户端确认的提交。
第三步:检查位点。新主提升前,比较旧主的提交位点和备库的应用位点。两者之间的差集就是潜在丢失窗口,也就是这次切换的 RPO 下界。异步复制下这个窗口通常不是零。
第四步:阻止旧主写入。旧主恢复后,不能因为进程健康就重新加入。它需要被标记为陈旧节点,等待重新同步;存储层最好有 fencing token 或任期号,拒绝旧权威的写。
第五步:定义读语义。切换期间客户端可能读到旧主、新主或副本。会话令牌、单调读和读己之写决定了用户会看到什么样的状态跳变。
第六步:验证与回切。切换完成后,用不变量检查扫一遍订单状态,确认没有回退记录;回切前重复同样的位点检查,避免把新主已确认的写丢在旧主之外。
这个例子说明,切换的可靠性不取决于脚本写得多顺,而取决于六个定义:不变量、权威、位点、写拒绝、读语义和验证。缺一个,切换就只是一次“看起来成功”的操作。
本书的证据从哪里来
卷三的结论按证据强度分级。读的时候可以先看这一节,判断某个说法属于哪一级。
| 证据类型 | 能证明什么 | 不能证明什么 |
|---|---|---|
| 单元测试 | 某个状态机或算法在给定输入下的行为 | 真实网络下的表现 |
| 确定性多节点模拟 | 协议逻辑、消息顺序、分区与恢复路径 | 吞吐、延迟和工程级稳定性 |
| 历史记录检查器 | 一次并发执行是否满足某种一致性模型 | 所有可能的执行 |
| 故障注入 | 指定故障下的不变量是否守住 | 未注入的故障组合 |
| 真实依赖测试 | 数据库、缓存、消息中间件的实际行为 | 生产规模和数据分布 |
| 生产演练 | 切换、回切和容灾流程是否可执行 | 未演练到的故障 |
前三类是卷三的主力。它们可复现、成本低,适合验证“逻辑是否正确”。后三类成本高,只在关键章节出现,并且必须写明环境、版本和未执行的部分。
三类主力证据之间不是替代关系。单元测试证明单个组件的局部行为,多节点模拟证明协议在指定消息序列下的行为,历史记录检查器证明一次并发执行是否满足某个一致性模型。三者合起来,才能覆盖“组件正确、协议正确、执行可判定”三个层次。
反过来,任何一层缺失都会让结论变弱。只有单元测试,无法说明节点之间如何交互;只有模拟,无法说明真实存储的语义;只有检查器,无法说明实现是否真的产生了被检查的历史。
如果某一节只给出架构图而没有可运行的检查,读者应该把它当作设计推演,而不是已验证的结论。
与前两卷的衔接
卷三默认读者已经具备卷一的边界意识和卷二的测量意识。没有这两层基础,直接读分布式协议容易变成背名词。
- 不清楚聚合、端口与适配器,先回看模块化单体 和端口与适配器边界 ;
- 不清楚事务边界与 Outbox 的起点,先回看事务边界与 Outbox ;
- 不清楚故障域和接管条件,先回看冗余与故障域 ;
- 不清楚超时、重试与幂等的关系,先回看超时预算 和重试放大 ;
- 想知道卷二如何把问题交给卷三,可以读进入卷三 。
这些链接不是前置课程,而是回查入口。读卷三卡住时,回到对应章节补一个概念,比在原地反复读同一段更有效。
读完卷三应该能做什么
读完并做过练习之后,读者应该能够:
- 把一个模糊的“数据要一致”拆成若干条可检查的业务不变量;
- 判断一次跨节点调用处于成功、失败、未知还是部分成功,并设计收敛路径;
- 解释单主复制、异步复制和故障切换的丢数据窗口,写出 RPO 与切换检查表;
- 说清 Raft 的角色、任期、日志匹配和提交规则,知道迷你实现与生产实现的差距;
- 为一个订单系统选择分片键,识别热点,设计可重入的迁移与回滚;
- 在 2PC、Saga、TCC、Outbox 之间做有依据的选择,并说明各自的失败模式;
- 设计幂等键、去重表和对账任务,让重复与差异能够被自动收敛;
- 判断一把分布式锁在租约过期、主从切换和时钟回拨下是否还安全;
- 为单元化、双活或多活拓扑写出故障隔离边界、数据归属和演练方案;
- 用故障注入和不变量检查验证上述设计,而不是只写文档。
最后
分布式与数据一致性不是两个附加题,而是同一个问题的两面:当系统由多个会失败的节点组成时,业务规则如何仍然成立。只谈分布式不谈一致性,会得到一套能扩容但数据会乱的系统;只谈一致性不谈分布式,会得到一套在单机上正确、跨节点就失效的方案。
卷三的目标不是让你记住更多协议,而是让你在面对一次跨库操作时,能回答五个问题:业务动作的边界在哪里,每一步的结果是否可查询,重复执行会不会产生第二个副作用,失败后是继续、补偿还是人工介入,最终由谁判断数据已经一致。
能回答这五个问题,比记住任何“最佳实践”都更有用。
分布式系统最危险的地方,不是它复杂,而是它经常在大部分时间里表现得很正常。正常路径跑得越顺,团队越容易把“没有报错”当成“没有故障”。卷三反复做故障注入和对账,就是为了在正常时间里暴露那些只会在异常时间出现的问题。
读这本书不需要一次记住所有协议。更好的方式是把每一章当成一次检查:如果你的系统明天要做跨库下单、主备切换或分片扩容,这一章能帮你问出哪些问题,能提供哪一个可运行的验证。
这本书不要求你同意每一个结论。它要求的是:当你不同意时,能指出是哪条不变量、哪个故障假设、哪次执行没有被覆盖。
分布式系统的讨论最容易停在术语层面。把它拉回可运行的检查,是卷三最想提供的训练。
从 1.1 开始。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。