第 8 章:重量级方案——TiDB 与 CockroachDB——从零打造分布式
结论先行:TiDB 和 CockroachDB 都是”从零开始”设计的原生分布式数据库,但它们走了完全不同的路——TiDB 选择了存算分离 + HTAP 双引擎,CockroachDB 选择了存算一体 + 强一致性。理解这两条路径的设计哲学和 tradeoff,能帮你理解分布式数据库设计中最深层的决策。
两家餐厅的故事
TiDB 像一家大型连锁餐厅集团。前厅(tidb-server/SQL 层)负责点菜和客户服务,后厨(TiKV)负责烹饪,甜品工坊(TiFlash)负责做蛋糕和甜点。三个部门各自独立运营,通过物流系统(PD 调度器)协调。需要更多服务员?加前厅。需要更快的出菜速度?加后厨。需要分析数据做经营报表?甜品工坊的团队同时也能帮忙(TiFlash 列存分析)。
CockroachDB 像一个”全能厨师”连锁店。每家店只有一个厨师,但这个厨师从接待客人、点菜、烹饪、上菜到结账全部搞定。任何一家店关门,客人去隔壁照常吃饭。不需要中央厨房(没有单点),不需要物流协调(没有独立的调度器),每家店完全独立。
TiDB 深度剖析
架构哲学:存算分离
TiDB 的核心设计决策是存算分离——计算和存储在不同的进程(甚至不同的机器)上运行,可以独立扩展。
TiDB 集群全景:
┌─────────────────────────────────────────────────┐
│ TiDB Server 集群 │
│ tidb-server1 tidb-server2 tidb-server3 │
│ (Go, SQL层) (Go, SQL层) (Go, SQL层) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Parser │ │ Parser │ │ Parser │ │
│ │ Cascades │ │ Cascades │ │ Cascades │ │
│ │ Executor │ │ Executor │ │ Executor │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
└───────┼──────────────┼──────────────┼───────────┘
│ │ │
┌───────┼──────────────┼──────────────┼───────────┐
│ └──────────────┼──────────────┘ │
│ │ │
│ PD (Placement Driver) │
│ Region 调度 / 负载均衡 / TSO │
│ │ │
│ ┌──────────────┼──────────────┐ │
│ │ │ │ │
│ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ │
│ │ TiKV 1 │ │ TiKV 2 │ │ TiKV 3 │ │
│ │ (Rust) │ │ (Rust) │ │ (Rust) │ │
│ │ 行存 │ │ 行存 │ │ 行存 │ │
│ │ RocksDB │ │ RocksDB │ │ RocksDB │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │Raft │Raft │Raft │
│ │Learner │Learner │Learner │
│ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ │
│ │TiFlash 1│ │TiFlash 2│ │TiFlash 3│ │
│ │ (C++) │ │ (C++) │ │ (C++) │ │
│ │ 列存 │ │ 列存 │ │ 列存 │ │
│ │ Delta │ │ Delta │ │ Delta │ │
│ └─────────┘ └─────────┘ └─────────┘ │
└────────────────────────────────────────────────┘
Raft Learner HTAP:TiDB 最精彩的设计
第 4 章已经介绍了 Raft Learner 的基本原理。这里我们深入看 TiDB 如何用它实现 HTAP。
传统做 HTAP 有两种方式,都有明显的缺点:
传统方式一:ETL(每晚批量导出)
白天: OLTP 数据库处理事务
深夜: 把数据导出(ETL)到分析数据库
问题: 分析数据永远是"昨天的"
传统方式二:同一个引擎跑两种负载
一台机器同时跑事务和分析
问题: 分析查询(大量扫描)会严重影响事务处理(低延迟要求)
TiDB 的方式:Raft Learner + 双存储引擎
TiKV 处理事务(行存,低延迟随机读写)
TiFlash 处理分析(列存,高吞吐顺序扫描)
两者通过 Raft Learner 实时同步(秒级延迟)
互不干扰(不同进程,不同机器)
查询优化器怎么决定用 TiKV 还是 TiFlash?这就是 Cascades 框架发挥作用的地方——优化器在生成执行计划时,会同时评估”下推到 TiKV”和”下推到 TiFlash”的成本,选成本更低的方案。
-- 查询示例
SELECT city, COUNT(*), AVG(amount)
FROM orders
WHERE order_date > '2026-01-01'
GROUP BY city;
-- 优化器的决策过程:
-- 方案A:下推到 TiKV(行存)
-- 需要扫描所有行,逐行检查 order_date,逐行聚合
-- 估算成本:高(行存不擅长大范围扫描和聚合)
-- 方案B:下推到 TiFlash(列存)
-- 只读 order_date 和 city 和 amount 三列
-- 列存压缩后数据量小,顺序扫描快
-- 估算成本:低 ← 选择这个方案
Cascades 优化器的实现细节
TiDB 的 Cascades 实现在 pkg/planner/cascades/cascades.go:
Cascades 优化器的三个核心组件:
1. Memo(记忆宫殿)
存储所有探索过的等价执行计划
避免重复计算
2. Rule Engine(规则引擎)
转换规则:把一种执行方式转换成另一种等价方式
例如:InnerJoin(A, B) → InnerJoin(B, A)(交换律)
3. Scheduler(调度器)
决定下一步探索哪个 Group、应用哪条规则
目标:以最少的探索找到成本最低的计划
规则引擎中有几十条优化规则,关键的包括:
谓词下推(Predicate Pushdown):
Filter(Join(A, B), A.x > 10)
→ Join(Filter(A, A.x > 10), B)
效果:减少 JOIN 的输入行数
JOIN 重排序(Join Reorder):
Join(Join(A, B), C)
→ Join(A, Join(B, C))
效果:可能找到更优的 JOIN 顺序
聚合消除(Aggregation Elimination):
Aggregate(唯一键分组)
→ 直接返回(分组键唯一意味着不需要真正聚合)
效果:减少不必要的计算
多仓库生态
TiDB 的一个显著特点是多仓库架构——不同组件在不同的 Git 仓库中,用不同的编程语言开发:
pingcap/tidb → Go → SQL 层(解析、优化、执行)
tikv/tikv → Rust → KV 存储层(Raft、RocksDB)
tikv/pd → Go → 调度器(Region 管理、负载均衡)
pingcap/tiflash → C++ → 列存引擎
优点:
- 各组件可以独立开发、测试、部署
- 用最适合的语言(Rust 写存储、Go 写 SQL 层)
缺点:
- 跨仓库调试复杂
- 版本兼容性管理困难
- 新贡献者需要了解多个仓库
CockroachDB 深度剖析
架构哲学:存算一体
CockroachDB 的核心设计决策是存算一体——每个节点都是一个完整的数据库,既能处理 SQL 又能存储数据。没有单独的存储层、没有单独的调度器、没有单点。
CockroachDB 集群(每个节点结构完全相同):
┌─────────────────────────────────────────┐
│ CockroachDB Node │
│ ┌───────────────────────────────────┐ │
│ │ SQL Layer │ │
│ │ ├── Parser (PG 兼容语法) │ │
│ │ ├── Optimizer (基于代价) │ │
│ │ └── DistSQL Execution │ │
│ ├───────────────────────────────────┤ │
│ │ Transaction Layer │ │
│ │ ├── MVCC (多版本并发控制) │ │
│ │ ├── Latch Manager (本地锁) │ │
│ │ └── TxnCoordSender (事务协调) │ │
│ ├───────────────────────────────────┤ │
│ │ Distribution Layer │ │
│ │ ├── DistSender (Range 路由) │ │
│ │ ├── Raft (共识复制) │ │
│ │ └── Range 管理 (分裂/合并/迁移) │ │
│ ├───────────────────────────────────┤ │
│ │ Storage Layer (Pebble) │ │
│ │ └── LSM-Tree KV 存储 │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
所有节点结构完全相同 → 对等架构
没有 master/slave → 没有单点
Parallel Commits:减少一轮 RTT
CockroachDB 对分布式事务的一个重大优化是 Parallel Commits——把传统 2PC 的两轮网络往返(prepare + commit)压缩到一轮。
传统两阶段提交:
传统 2PC(两轮 RTT):
协调者 参与者
│ │
│── Phase 1: Prepare ──► │
│◄── 我准备好了 ───────── │
│ │ 第一轮 RTT(~1ms)
│── Phase 2: Commit! ──► │
│◄── 提交完成 ────────── │ 第二轮 RTT(~1ms)
│ │
总延迟 ≈ 2ms(2轮 RTT)
CockroachDB 的 Parallel Commits:
Parallel Commits(一轮 RTT):
协调者 参与者
│ │
│── 写数据 + 标记 STAGING ──► │
│ │ 一轮 RTT(~1ms)
│◄── 确认 ──────────────────── │
│ │
事务状态从 STAGING → COMMITTED
(异步完成,不需要额外 RTT)
总延迟 ≈ 1ms(1轮 RTT)
关键创新:事务的最终写操作和 commit 决策并行发出。协调者把事务标记为 STAGING 状态(”如果所有写都成功了,这个事务就算提交”),然后并行地发出所有写操作。当所有写操作的确认回来后,事务自动从 STAGING 变成 COMMITTED——不需要额外的 commit 轮次。
CockroachDB 用 TLA+ 形式化验证了这个协议的正确性(docs/tla-plus/ParallelCommits/ParallelCommits.tla)。这不是简单的测试——TLA+ 会穷举所有可能的状态组合(包括网络分区、节点崩溃等极端场景),数学证明协议在所有情况下都是正确的。
Pebble:自研存储引擎
CockroachDB 最初使用 RocksDB(Facebook 开源的 LSM-Tree 引擎)作为底层存储,后来自研了 Pebble 来替代。
为什么要自研?
RocksDB 的问题(对 CockroachDB 来说):
1. C++ 代码,Go 通过 CGo 调用 → 跨语言调用开销
2. 丰富的功能(Column Family、Compaction Filter 等)
CockroachDB 用不到,但仍然要承担复杂度
3. Go 的内存管理和 C++ 的内存管理不协调
Pebble 的优势:
1. 纯 Go 实现 → 消除 CGo 开销
2. 针对 CockroachDB 的使用模式优化
3. 更好地集成 Go 的内存管理和 GC
Pebble 是一个完整的 LSM-Tree 实现,性能已经超过了 RocksDB(在 CockroachDB 的使用场景下)。
Serializable 默认隔离级别
CockroachDB 做了一个大胆的设计决策:默认使用 Serializable 隔离级别。这是 SQL 标准定义的最严格隔离级别——事务的执行结果等价于某种串行顺序。
大多数数据库(包括 PostgreSQL 和 MySQL)默认使用较弱的隔离级别(如 Read Committed 或 Repeatable Read),因为 Serializable 的性能开销更大。CockroachDB 认为:正确性比性能更重要——绝大多数应用 bug 的根源是开发者不理解弱隔离级别的边界条件。
Serializable 保证的含义:
假设两个事务同时执行:
T1: 读取账户A余额(100),写入账户B余额(B+100)
T2: 读取账户B余额(0),写入账户A余额(A-100)
Serializable 保证结果等价于某种串行执行:
要么 T1 先执行再 T2:A=0, B=100 ✓ 正确
要么 T2 先执行再 T1:A=0, B=100 ✓ 正确
弱隔离级别可能产生的结果:
T1 和 T2 同时读到旧值:A=0, B=100 ... 还是正确?
不一定!某些交叉执行可能导致 A=0, B=0(钱消失了)
代价是:当 CockroachDB 检测到两个事务可能违反 Serializable 时,它会自动中止并重试其中一个事务。应用需要做好重试逻辑。
TiDB vs CockroachDB:深层对比
存算分离 vs 存算一体
| 维度 | TiDB(存算分离) | CockroachDB(存算一体) |
|---|---|---|
| 扩展方式 | 计算和存储独立扩展 | 整体扩展 |
| 弹性 | 高(可以只加计算节点) | 中(加节点=加计算+存储) |
| 网络依赖 | 高(计算和存储之间有网络) | 低(本地访问) |
| 单查询延迟 | 较高(跨网络访问存储) | 较低(本地存储访问) |
| 故障域 | 更细粒度(计算层故障不影响存储) | 整节点故障 |
| 运维复杂度 | 高(多种组件) | 低(所有节点相同) |
HTAP 能力对比
TiDB 的 HTAP:
┌─────────┐ Raft Learner ┌─────────┐
│ TiKV │ ────────────────► │ TiFlash │
│ (行存) │ 秒级同步 │ (列存) │
│ OLTP │ │ OLAP │
└─────────┘ └─────────┘
两个独立引擎,物理隔离
CockroachDB 的"HTAP":
┌──────────────────┐
│ Pebble │
│ (行存 only) │
│ OLTP ✓ OLAP △ │
└──────────────────┘
一个引擎,OLAP 能力有限
TiDB 在 HTAP 上有明显优势——独立的列存引擎 TiFlash 让分析查询不影响事务处理。CockroachDB 不支持列存,复杂分析查询性能有限。
一致性模型对比
TiDB 默认: Snapshot Isolation (SI)
- 每个事务看到一个一致的数据快照
- 允许 Write Skew 异常(两个事务基于同一快照做不同的写入)
- 性能较好
CockroachDB 默认: Serializable
- 事务结果等价于串行执行
- 不允许任何异常
- 可能需要事务重试
工程文化对比
TiDB:
- 多仓库(tidb/tikv/pd/tiflash)
- Go + Rust + C++ 多语言
- 中文社区为主
- PingCAP 主导
CockroachDB:
- 单仓库(cockroachdb/cockroach)
- 纯 Go(Pebble 也是 Go)
- 英文社区
- 极致的测试文化(TLA+、Data-driven Test、Chaos Test)
- doc.go 文档详细程度惊人
学习价值对比
想理解分布式数据库的不同方面,两个项目各有优势:
想学... 推荐看...
─────────────────────────────────────────
HTAP 架构设计 TiDB(Raft Learner + TiFlash)
查询优化器框架 TiDB(完整 Cascades 实现)
分布式事务协议 CockroachDB(Parallel Commits + TLA+)
共识协议工程化 CockroachDB(深度定制 Raft)
存储引擎设计 CockroachDB(Pebble 自研)
形式化验证实践 CockroachDB(TLA+ 文件夹)
多语言微服务架构 TiDB(Go + Rust 协作)
常见误区
误区一:”原生分布式比 Citus/Vitess 好” → 正确理解:原生分布式能力更强,但复杂度也更高。部署一套 TiDB 集群(tidb-server + TiKV + PD + TiFlash)比部署 Citus(PG + Extension)复杂得多。如果你的需求 Citus 能满足,用 Citus 可能是更好的选择。
误区二:”TiDB 兼容 MySQL,迁移过去就行” → 正确理解:TiDB 的 MySQL 兼容性虽高但不是 100%。存储过程、触发器支持有限,某些 MySQL 特有的行为可能不同。迁移前需要做兼容性评估。
误区三:”CockroachDB 的 Serializable 默认会导致性能很差” → 正确理解:对于大多数 OLTP 负载,Serializable 的性能开销在 5-10% 左右——这是正确性换来的小代价。真正的性能影响在于事务冲突率——如果冲突率高,重试开销确实会很大。
读完本章你能做什么
- 用”餐厅”的类比解释 TiDB 存算分离和 CockroachDB 存算一体的区别
- 解释 Raft Learner 如何让 TiDB 实现不影响 OLTP 的实时分析
- 解释 Parallel Commits 如何把 2PC 的两轮 RTT 压缩到一轮
- 理解 Serializable 隔离级别的含义和 CockroachDB 选择它的原因
- 在 TiDB 和 CockroachDB 之间做出合理的技术选型
导读目录:README.md 上一章:第 7 章 · Citus 与 Vitess——不动内核的智慧 下一章:第 9 章 · YugabyteDB——一个引擎两种语言