在单机世界里,你以为理所当然的事情,到了分布式世界全部失效。网络会断、时钟会漂、节点会挂——欢迎来到真实的分布式世界。

一个转账的故事

假设你要实现一个银行转账功能:张三给李四转 100 元。

单机版本,简单得令人发指:

BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE user = '张三';
UPDATE accounts SET balance = balance + 100 WHERE user = '李四';
COMMIT;

数据库保证:要么两条都成功,要么两条都失败。这就是 ACID 中的 A(原子性)。

现在,假设张三的账户在北京机房,李四的账户在上海机房。

同样的转账,你需要:

  1. 北京机房:扣张三 100 元
  2. 上海机房:加李四 100 元

问题来了:如果北京扣款成功,但发给上海的消息丢了呢?

  • 张三少了 100 元
  • 李四没收到钱
  • 100 元凭空消失了

这不是假设,这是分布式系统的日常。

单机世界的三个假设

在单机世界里,数据库默默帮你解决了很多问题。它依赖三个隐含假设:

假设单机世界分布式世界
通信可靠内存读写不会失败网络随时可能断
时间一致只有一个时钟每台机器的时钟都不一样
节点可靠机器不会中途消失任何节点随时可能挂掉

分布式系统的本质困难:这三个假设全部失效。

假设一崩塌:网络不可靠

消息可能丢失

北京机房              上海机房
    │                    │
    │─── 扣款成功 ───────X  (消息丢了)
    │                    │
    │    (等待确认...)    │
    │                    │ (什么都没收到)

北京扣了钱,上海不知道。怎么办?

重试? 可能导致重复扣款。

不重试? 可能数据不一致。

消息可能重复

北京机房              上海机房
    │                    │
    │─── 加 100 元 ──────►│ (收到了)
    │                    │
    │  (没收到确认,重试)  │
    │                    │
    │─── 加 100 元 ──────►│ (又收到了)
    │                    │
    │                    │ 余额多了 100!

消息可能乱序

北京机房                        上海机房
    │                              │
    │─── 消息1 (通过连接A) ────────►│
    │─── 消息2 (通过连接B) ────────►│
    │                              │
    │                收到顺序:消息2, 消息1

注意:单个 TCP 连接保证顺序,但分布式系统常使用多连接、消息队列、或重试机制,这些都可能导致乱序。如果消息 1 是「创建订单」,消息 2 是「取消订单」,乱序会导致什么?

最可怕的:不知道对方是死是活

北京机房              上海机房
    │                    │
    │─── 请求 ───────────►│
    │                    │
    │    (等待响应...)    │
    │                    │
    │    3秒过去了...     │
    │                    │
    │    对方是:          │
    │    A) 挂了?        │
    │    B) 太忙?        │
    │    C) 网络断了?    │
    │    D) 响应在路上?  │

你无法区分「对方挂了」和「网络慢」。 这是分布式系统最根本的困难之一。

假设二崩塌:时钟不一致

在单机上,时间是确定的。在分布式系统中,每台机器都有自己的时钟。

物理时钟会漂移

两台机器的时钟,即使同时校准,几小时后也会产生几十毫秒的偏差。

这会导致什么问题?

假设用时间戳判断「谁先谁后」:

真实时间线:
  T=0ms:  A 写入 x=1
  T=20ms: B 写入 x=2(发生在 A 之后)

各机器记录的时间戳:
  机器 A (时钟快 50ms): 记录 10:00:00.050
  机器 B (时钟准):      记录 10:00:00.020

结果:
  按时间戳:A 更新(10:00:00.050 > 10:00:00.020)
  按真实时间:B 更新(B 发生在 A 之后 20ms)

时间戳骗了你。 A 的时钟快,导致 A 的时间戳虽然更大,但它实际上发生得更早。

NTP 同步也不可靠

NTP(网络时间协议)可以同步时钟,但:

  • 同步本身有网络延迟
  • 精度因网络环境而异:局域网可达亚毫秒,广域网通常在几十毫秒级别
  • 同步可能失败,时钟可能跳变

在分布式系统中,你不能依赖物理时钟来判断事件顺序。

假设三崩塌:节点会挂

单机程序崩溃,大不了重启。分布式系统中,部分节点挂掉是常态。

脑裂问题

假设你有 3 台数据库,配置了自动故障转移(如 MySQL Group Replication):

正常状态:
    主节点 A ◄──── 从节点 B
        └──────── 从节点 C

网络分区后:
    [机房1]           [机房2]
    主节点 A          从节点 B
                      从节点 C

    B 和 C 联系不上 A
    自动故障转移机制认为 A 挂了
    选举 B 为新主节点

    现在有两个主节点!

两个主节点都在接受写入,数据开始分叉。这就是「脑裂」。

网络恢复后,怎么合并两边的数据?没有通用答案。

僵尸节点

节点 A 以为自己还是主节点
    │  (实际上它已经被网络隔离)
    │  (其他节点已经选了新主节点)
继续接受写入
数据与集群不一致

一个节点可能不知道自己已经「死了」。

一个真实的例子

让我们回到开头的转账问题,看看分布式世界会发生什么:

北京机房                      上海机房
张三余额=1000                  李四余额=500
    │                            │
    │  [1] 扣张三 100 元           │
    │  张三余额=900               │
    │                            │
    │─── 通知上海:加李四 100 ────►│
    │                            │
    │         (网络延迟 2 秒)      │
    │                            │
    │  此时有人查询:              │
    │  张三=900, 李四=500         │
    │  总额少了 100!             │
    │                            │
    │                      [2] 加李四 100 元
    │                      李四余额=600

在那 2 秒的窗口期,系统处于「不一致」状态。

更糟的情况:如果网络消息丢失,上海永远收不到通知,100 元就真的消失了。

为什么不能简单地「加锁」?

在单机上,你可以用锁来保证一致性:

let _lock = mutex.lock();
// 临界区操作
// 自动释放锁

在分布式系统中,锁也是分布式的:

节点 A: 我要加锁
    ├─► 锁服务: OK,锁给你了
    │   (A 开始干活)
    │   (A 和锁服务之间网络断了)
    │   锁服务: A 好久没心跳了,锁过期,释放
    │   节点 B: 我要加锁
    │   锁服务: OK,锁给你了
    │   (B 开始干活)
    │   (A 的网络恢复了)
    │   A: 我还拿着锁呢,继续干活
    A 和 B 都认为自己持有锁!

分布式锁也不能解决根本问题。

单机 vs 分布式:对比总结

维度单机分布式
通信内存读写,纳秒级,100% 可靠网络调用,毫秒级,可能失败
时间单一时钟,绝对可信多个时钟,各有偏差
故障整体挂或整体活部分节点挂,部分节点活
事务数据库原生支持 ACID需要额外协议(2PC、Saga…)
进程内互斥,简单可靠分布式锁,可能失效
顺序程序顺序 = 执行顺序无全局顺序,需要额外机制

那怎么办?

面对这些困难,计算机科学家们发明了各种协议和算法:

问题解决方案
跨节点事务两阶段提交(2PC)、Saga 模式
一致性 vs 可用性CAP 定理指导下的权衡
多节点达成共识Paxos、Raft 算法
无需强一致的场景最终一致性、CRDT

没有银弹。 每种方案都是在一致性、可用性、性能之间做权衡。

总结

为什么分布式一致性这么难?

因为你习以为常的三个假设全部失效了:

  1. 网络不可靠:消息会丢、会重复、会乱序,你分不清对方是死是活
  2. 时钟不一致:每台机器的时钟都不一样,你不能用时间戳判断先后
  3. 节点会挂:部分节点随时可能失联,可能出现脑裂和僵尸节点

核心认知

分布式系统不是「多台机器组成的单机」,而是一个充满不确定性的环境。

你需要假设一切都会出错,然后设计系统在出错时仍能正确工作。

这是一种完全不同的思维方式。


下一篇,我们来看分布式系统的第一个尝试:两阶段提交(2PC)——它试图在分布式环境中实现 ACID,结果发现了 CAP 定理这个「不可能三角」。


下一篇:2PC 与 CAP:理想的破灭

本系列:

  1. 单机到分布式:一致性为何变难(本篇)
  2. 2PC 与 CAP:理想的破灭
  3. Paxos 与 Raft:让多数人达成共识
  4. 最终一致性:不强求,但终会一致
  5. CRDT:无需协调的合并魔法
  6. 现代方案:从 Spanner 到 TiDB
  7. 实战篇:方案选型与落地