第 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)之间选一个。
这不是理论家的空想——这是一个已经被数学证明的不可能性。让我们用图书馆的例子具体看看为什么:
假设北京馆和上海馆之间的网络断了。这时候一位读者在北京馆还了《三国演义》。
- 如果你选择一致性(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%(可能丢包、断开)
这个物理约束决定了分布式数据库设计中的每一个取舍:
- 为什么 CockroachDB 要设计 Parallel Commits?因为减少一轮网络往返就能节省几毫秒延迟。
- 为什么 OpenTenBase 的 GTM 是性能瓶颈?因为所有事务都要通过网络请求 GTM 获取全局事务 ID。
- 为什么 Citus 的五层规划器要”命中即停”?因为尽可能把查询限制在单个节点上执行,避免跨节点通信。
- 为什么 TiDB 用 Raft Learner 做 HTAP?因为让分析查询读 Learner 副本,避免干扰事务处理的主副本。
分布式数据库解决了什么
总结一下,分布式数据库解决了三个核心问题:
问题一:数据量超过单机容量 → 通过数据分片(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 只在网络分区发生时才强制二选一。在正常情况下,好的分布式数据库可以同时提供强一致性和高可用性。而且”一致性”和”可用性”都是连续光谱而非二元选择。
读完本章你能做什么
- 用”搬家”的类比向朋友解释垂直扩展和水平扩展的区别
- 说出单机数据库面临的三面墙(容量、性能、可用性),以及分布式数据库如何应对
- 用”连锁图书馆”的例子解释 CAP 定理
- 一句话概括 6 个分布式数据库项目各自的核心策略
- 理解分布式环境下 ACID 每个字母变难的原因
章节自测
- 你的电商网站日均 PV 从 1 万涨到 1000 万。你会选择垂直扩展还是水平扩展?在什么拐点上你必须从前者切换到后者?
- 假设你设计一个”全球用户注册系统”,用户在美国注册后 1 秒内就要在日本能查到。用 CAP 定理分析:你要放弃什么?有没有折中方案?
- 六个数据库中,TiDB 选择兼容 MySQL 协议,CockroachDB 选择兼容 PostgreSQL。这个”协议兼容”的设计决策带来了什么好处和什么代价?
导读目录:README.md 下一章:第 2 章 · 阅读路线图与前置知识