犀牛鸟 2026 研究笔记 estelledc.github.io

第 1 章:为什么需要分布式数据库——当一台机器装不下所有数据

结论先行:分布式数据库不是”更好的数据库”,而是”不得不用的数据库”。当你的数据量、访问量或可用性要求超过单台机器的能力极限时,你才需要分布式数据库。理解这个”不得已”,是理解后面所有设计取舍的起点。


一个双十一的故事

2024 年双十一,某电商平台的秒杀活动在零点开始。100 万用户同时点击”抢购”按钮,每次点击都会触发一连串数据库操作:查库存、扣库存、创建订单、扣减余额。

假设每次点击产生 5 条 SQL 语句,100 万用户就是 500 万条 SQL 在同一秒内涌入数据库。一台顶配的单机 MySQL 服务器——64 核 CPU、512GB 内存、NVMe SSD——每秒大约能处理 5 万到 10 万条简单 SQL。500 万条请求,至少需要 50 台这样的机器同时工作。

但问题来了:这 50 台机器上的数据怎么保持一致?用户 A 在机器 1 上扣了库存,用户 B 在机器 2 上查库存时能看到最新的数量吗?如果机器 3 突然宕机,上面正在处理的 1000 个订单怎么办?

这就是分布式数据库要解决的问题。

单机数据库:一个人搬家

在理解分布式数据库之前,我们先理解单机数据库为什么会”不够用”。

想象你一个人住在一间 20 平米的单间里。你的衣服、书籍、厨具都放在这间房里。只要东西不多,一个人完全能管理好——找东西快,整理方便,不需要跟任何人协调。

这就是单机数据库的状态。MySQL、PostgreSQL、SQLite,当数据量在几十 GB 以内、并发连接在几百个以内时,它们运行得非常好。查询快、事务可靠、维护简单。

但是当你的东西越来越多——衣服堆到天花板、书架放不下、厨房转不开身——你有两个选择:

选择一:换一间更大的房子(垂直扩展 / Scale Up)

买更好的 CPU、加更大的内存、换更快的硬盘。这就像搬到一间 100 平米的大房子里——问题暂时解决了,但代价高昂,而且房子有面积上限。一台服务器最多装几 TB 内存、几十 TB 硬盘,再往上就没有更大的”房子”了。

垂直扩展(Scale Up):
  8核/32GB  →  32核/128GB  →  64核/512GB  →  ???
  ¥5,000      ¥20,000       ¥100,000      没有更大的了

选择二:搬到多间房子里,分工合作(水平扩展 / Scale Out)

把衣服放一间房、书籍放一间房、厨具放一间房。每间房有自己的管理员,互相通过对讲机协调。需要更多空间?再加一间房就行。

水平扩展(Scale Out):
  机器1(用户A-M的数据)
  机器2(用户N-Z的数据)      需要更多?
  机器3(订单数据)        →   再加一台就行
  ...

水平扩展的理论上限几乎没有——Google 的 Spanner 跑在全球几百个数据中心的几百万台机器上。但代价是:你需要一套复杂的协调机制来保证多间房子里的东西不会乱套。

这套协调机制,就是分布式数据库。

单机数据库的三面墙

单机数据库会碰到三面”墙”。当你撞上其中任何一面,就是考虑分布式数据库的时候。

第一面墙:容量墙

一台服务器的硬盘空间是有限的。当你的数据量增长到单台机器装不下的程度(比如几十 TB 到 PB 级别),垂直扩展已经没有空间了。

真实案例:微信支付每天产生数十亿条交易记录。单机 PostgreSQL 就算用最大的硬盘也装不下一个月的数据。OpenTenBase 通过 Coordinator-Datanode 架构,把数据分散到多个 Datanode 上,每个 Datanode 只存一部分数据。

  用户发起支付
       |
  Coordinator(路由层)
   /    |    \
  DN1   DN2   DN3     ← 每个 Datanode 存一部分数据
 用户   用户   用户
 A-M    N-T    U-Z

第二面墙:性能墙

一台机器的 CPU 核数和内存带宽是有限的。当并发请求量超过单机处理能力时(比如每秒几万到几十万个事务),排队等待的请求越来越多,响应时间急剧上升。

真实案例:YouTube 在 2010 年代初期,MySQL 单实例每天处理数亿次查询。当用户量继续增长时,单个 MySQL 实例的连接数成为瓶颈。Vitess 在 MySQL 外面加了一层代理(VTGate),把请求分散到多个 MySQL 实例上,每个实例只处理一部分用户的查询。

  10万用户同时查询
         |
     VTGate(路由代理)
    /    |    \    \
  MySQL  MySQL MySQL MySQL    ← 每个 MySQL 只处理一部分请求
  shard1 shard2 shard3 shard4

第三面墙:可用性墙

再好的机器也会坏。硬盘故障、网络中断、机房断电——这些事情不是”会不会发生”的问题,而是”什么时候发生”的问题。单机数据库宕机意味着整个系统不可用,对于银行、电商、支付这类系统,哪怕停机 1 分钟都可能造成巨大损失。

真实案例:CockroachDB 的设计目标就是”打不死的蟑螂”。每份数据默认存 3 个副本在不同的机器上(甚至不同的数据中心)。任何一台机器宕机,系统自动切换到其他副本继续服务,用户完全感知不到。

  数据 "订单#12345"
       |
   ┌───┼───┐
   v   v   v
  副本1 副本2 副本3     ← 存在 3 台不同的机器上
  (北京) (上海) (广州)

  北京机器宕机?上海和广州的副本继续服务。

CAP 定理:一个不可能三角

在理解了”为什么需要分布式”之后,你需要理解分布式数据库设计中最重要的理论约束——CAP 定理。

想象你在管理一个跨三个城市的连锁图书馆:北京馆、上海馆、广州馆。每个馆都有一本”馆藏目录”,记录着所有图书的状态(在架、借出、预约等)。

你需要保证三件事:

C(Consistency / 一致性):三个馆的目录内容完全一样。张三在北京馆还了一本书,李四在上海馆查到的状态也立即变成”在架”。

A(Availability / 可用性):无论读者走进哪个馆,都能立刻查到目录。不能说”稍等,我正在跟其他馆同步呢”。

P(Partition Tolerance / 分区容忍性):即使三个馆之间的通信线路断了(比如光缆被挖断),每个馆仍然能独立运作。

CAP 定理说:在网络分区发生时,你只能在一致性(C)和可用性(A)之间选一个。

这不是理论家的空想——这是一个已经被数学证明的不可能性。让我们用图书馆的例子具体看看为什么:

假设北京馆和上海馆之间的网络断了。这时候一位读者在北京馆还了《三国演义》。

     正常情况           网络分区后
  ┌───────────┐      ┌───────────┐
  │ 北京馆    │      │ 北京馆    │
  │ 三国:在架 │◄────►│ 三国:在架 │
  └───────────┘      └─────┬─────┘
       ↕                   ✕ 网络断开
  ┌───────────┐      ┌─────┴─────┐
  │ 上海馆    │      │ 上海馆    │
  │ 三国:在架 │      │ 三国:借出  │ ← 过时信息
  └───────────┘      └───────────┘

六个项目在 CAP 取舍上的选择各不相同:

项目 CAP 倾向 具体策略
CockroachDB CP 默认 Serializable 隔离,宁可拒绝服务也不返回过时数据
TiDB CP Raft 共识保证强一致,少数派副本可能暂时不可用
YugabyteDB CP 类似 Spanner 的强一致设计
OpenTenBase CP GTM 中心化协调保证一致性
Vitess AP 倾向 作为中间件,一致性依赖底层 MySQL 的复制配置
Citus 取决于配置 作为 PG 扩展,2PC 模式下偏 CP,非 2PC 下偏 AP

注意:CAP 不是”三选二”那么简单。在没有网络分区的正常情况下,好的分布式数据库可以同时提供一致性和可用性。CAP 定理讨论的是网络分区发生时的取舍。

ACID 在分布式环境下的挑战

如果你学过数据库课程,一定听过 ACID——事务的四个保证。在单机数据库里,ACID 是”理所当然”的。但到了分布式环境下,每一个字母都变成了一道难题。

A(Atomicity / 原子性):要么全做完,要么全没做

单机场景:一个事务包含”扣库存”和”创建订单”两步操作。如果第二步失败了,第一步也要回滚——用一个本地的 WAL(Write-Ahead Log)就能搞定。

分布式场景:库存数据在机器 A 上,订单数据在机器 B 上。你需要让两台机器同时成功或同时失败。如果机器 A 已经扣了库存,但在通知机器 B 创建订单之前网络断了——怎么办?

这就是经典的”两阶段提交”(2PC)问题。后面你会看到,CockroachDB 用 Parallel Commits 把它优化到了极致,OpenTenBase 用 GTM 做中心化协调。

传统两阶段提交(2PC):

  协调者                 参与者A        参与者B
    |--- Prepare? -------->|              |
    |--- Prepare? ----------------------->|
    |<-- Ready ------------|              |
    |<-- Ready ----------------------------|
    |--- Commit! --------->|              |
    |--- Commit! ------------------------->|
    
    两轮网络往返 = 高延迟

C(Consistency / 一致性):数据始终满足约束

单机场景:外键约束、唯一约束、CHECK 约束——数据库在一台机器上就能检查所有这些。

分布式场景:如果唯一约束要求”订单号全局唯一”,但订单分散在 10 台机器上——怎么检查全局唯一性?每次插入订单都要查遍所有 10 台机器吗?那太慢了。

这就是为什么很多分布式数据库对外键的支持不完整。TiDB 和 CockroachDB 支持外键但性能有损失,Vitess 直接建议”不要在分布式场景用外键”。

I(Isolation / 隔离性):并发事务互不干扰

单机场景:锁机制可以保证事务之间不互相干扰。

分布式场景:事务 T1 在机器 A 上读数据,事务 T2 在机器 B 上写同一份数据的另一个副本——T1 应该看到 T2 的修改吗?这取决于”隔离级别”,而在分布式环境下实现高隔离级别的成本远高于单机。

CockroachDB 默认使用最严格的 Serializable 隔离级别(代价是性能),TiDB 默认使用 Snapshot Isolation(在一致性和性能之间取平衡),OpenTenBase 的 GTM 提供全局快照来支撑隔离性。

D(Durability / 持久性):提交的数据不会丢失

单机场景:写入磁盘(WAL + 数据文件)后,即使断电也不丢数据。

分布式场景:数据写入机器 A 的磁盘后,机器 A 的硬盘坏了怎么办?所以分布式数据库必须把数据复制到多台机器。但复制带来新问题:写入 3 个副本,是等 3 个都确认(安全但慢),还是等 1 个确认(快但可能丢数据)?

Raft 协议的”多数派确认”是一个精巧的折中:3 个副本等 2 个确认。这样即使 1 台机器宕机数据也不丢,而且不需要等最慢的那台。

Raft 多数派确认:

  Leader 收到写请求
    |
    ├──发送给 Follower1 ──► 确认 ✓
    ├──发送给 Follower2 ──► 确认 ✓   ← 2/3 确认,提交!
    └──发送给 Follower3 ──► 网络延迟...
    
  不需要等 Follower3,2/3 已经是多数派

从单机到分布式:不只是”加几台机器”

很多初学者会觉得,分布式数据库就是”把数据分散到多台机器上”。这只是最表面的一层。真正的挑战是:如何让多台机器看起来像一台机器一样工作。

单机数据库有一个巨大的隐含优势:共享内存。所有的事务、锁、缓存、日志都在同一片内存空间里,协调成本几乎为零。

分布式环境下,这个优势消失了。取而代之的是网络通信——网络比内存慢 100 万倍,而且不可靠(会丢包、会延迟、会断开)。

单机:
  事务A ──读取──► 内存 ◄──写入── 事务B
           延迟: ~100 纳秒
           可靠性: 100%

分布式:
  事务A(机器1) ──网络──► 机器2 ◄──网络── 事务B(机器3)
                延迟: ~1 毫秒(= 1,000,000 纳秒)
                可靠性: <100%(可能丢包、断开)

这个物理约束决定了分布式数据库设计中的每一个取舍:

分布式数据库解决了什么

总结一下,分布式数据库解决了三个核心问题:

问题一:数据量超过单机容量 → 通过数据分片(Sharding)把数据分散到多台机器上。第 5 章会详细讲解不同的分片策略。

问题二:请求量超过单机处理能力 → 通过负载均衡把请求分散到多个节点处理。查询路由是关键,第 6 章会详细讲解。

问题三:单机故障导致服务中断 → 通过数据复制在多台机器上保留数据副本,故障时自动切换。第 4 章会详细讲解共识和复制机制。

但分布式数据库同时引入了新的复杂性:

单机很简单的事情 分布式变得很复杂 本导读哪章讲
事务 ACID 分布式事务一致性 第 4、12 章
查询优化 跨节点查询优化 第 6 章
加一列(DDL) 分布式 DDL 第 12 章
锁和隔离级别 全局锁和全局隔离 第 12 章
时间顺序 全局时钟 第 4 章
死锁检测 全局死锁检测 第 12 章

六个项目的一句话定位

在后续章节详细展开之前,先记住这六个项目各自解决问题的方式:

TiDB(约 41K Stars,Go):从零打造一个新的分布式数据库,兼容 MySQL 语法,通过 TiKV(行存)+ TiFlash(列存)同时处理事务和分析。

CockroachDB(约 32K Stars,Go):复刻 Google Spanner 的设计,兼容 PostgreSQL 语法,每个节点存算一体,强一致事务,自动分片。

Vitess(约 21K Stars,Go):不改 MySQL 一行代码,在外面加一层智能代理做分片路由,YouTube 十年大规模验证。

Citus(约 12.5K Stars,C):作为 PostgreSQL 的插件运行,不 fork 不魔改,用最小侵入实现分布式查询。

YugabyteDB(约 10.5K Stars,C/C++):基于 Spanner 架构,同时支持 PostgreSQL 和 Cassandra 两种查询语言。

OpenTenBase(约 2K Stars,C):腾讯开源,在 PostgreSQL 内核里做改造,通过 GTM 中心化协调实现分布式事务,支持行存和列存双引擎。

        架构谱系一览

           Google Spanner (论文)
            /              \
      CockroachDB        YugabyteDB
     (Go, 存算一体)    (C++, 双协议)

      PostgreSQL 内核              MySQL 内核
       /     |      \               /        \
    Citus  OpenTenBase  YSQL层   TiDB       Vitess
   (扩展)  (XL fork)   (YDB)  (原生分布式) (中间件代理)

常见误区

误区一:”分布式数据库一定比单机数据库快” → 正确理解:对于简单查询和小数据量,单机数据库几乎总是更快。分布式数据库的网络通信开销是真实存在的。分布式数据库的优势在于水平扩展能力,而不是单条查询的速度。

误区二:”用了分布式数据库就不用管分片策略” → 正确理解:即使是 CockroachDB 和 TiDB 这样的”自动分片”数据库,分片键的选择仍然至关重要。错误的分片键会导致数据热点——所有请求都涌向同一个节点,分布式变成了”单机 + 额外开销”。

误区三:”CAP 定理意味着分布式数据库必须放弃一致性或可用性” → 正确理解:CAP 只在网络分区发生时才强制二选一。在正常情况下,好的分布式数据库可以同时提供强一致性和高可用性。而且”一致性”和”可用性”都是连续光谱而非二元选择。

读完本章你能做什么

章节自测

  1. 你的电商网站日均 PV 从 1 万涨到 1000 万。你会选择垂直扩展还是水平扩展?在什么拐点上你必须从前者切换到后者?
  2. 假设你设计一个”全球用户注册系统”,用户在美国注册后 1 秒内就要在日本能查到。用 CAP 定理分析:你要放弃什么?有没有折中方案?
  3. 六个数据库中,TiDB 选择兼容 MySQL 协议,CockroachDB 选择兼容 PostgreSQL。这个”协议兼容”的设计决策带来了什么好处和什么代价?

导读目录:README.md 下一章:第 2 章 · 阅读路线图与前置知识