TiDB:一个“三头六臂”的分布式数据库,如何把 MySQL 拆了又拼起来

3 阅读11分钟

TiDB:一个“三头六臂”的分布式数据库,如何把 MySQL 拆了又拼起来?🧩🐬

朋友们,想象一下:MySQL 是个老实人,一个人默默扛下所有数据。突然有一天,数据量爆炸增长,MySQL 累趴下了。你说:“分库分表吧!”结果 MySQL 被大卸八块,每个部分都成了“残疾”——不能 JOIN、不能事务、运维要命。😫

这时,TiDB 拍拍胸脯说:“别拆了!让我来组建一个‘复仇者联盟’,每个成员各司其职,还能默契配合!”今天,我们就来揭秘这个“数据库联盟”的内部运作。🚀


一、TiDB 的“人格分裂”:三头六臂的架构 🎭

1. 三个“人格”分工明确

人格1:TiDB Server(计算层) ​ - “聪明的前台接待” 👨💼

  • 职责:接 SQL 请求、算执行计划、返回结果
  • 特点无状态,随便扩,不存数据
  • 口头禅:“SQL 交给我,数据在哪我帮你找!”

人格2:PD Server(调度层) ​ - “全能的项目经理” 📊

  • 职责:管元数据、调度数据分布、分配时间戳
  • 特点集群大脑,奇数个(防脑裂)
  • 口头禅:“数据放哪我最清楚,听我调度!”

人格3:TiKV Server(存储层) ​ - “勤劳的仓库管理员” 📦

  • 职责:存数据、保证一致性、处理事务
  • 特点有状态,存真实数据,用 Raft 保证一致
  • 口头禅:“数据在我这,绝对安全可靠!”

2. 架构全景图

用户请求
    ↓
┌─────────────────────────────────────┐
│        TiDB Server (计算层)         │ ← 无状态,可水平扩展
│  ┌─────────────────────────────┐  │
│  │ 1. SQL解析器                │  │
│  │ 2. 优化器(选最便宜的执行计划)│  │
│  │ 3. 执行器(干活!)         │  │
│  └─────────────────────────────┘  │
└───────────────┬────────────────────┘
                │ 问PD:数据在哪?
                ↓
┌─────────────────────────────────────┐
│        PD Server (调度层)           │ ← 集群大脑
│  ┌─────────────────────────────┐  │
│  │ 1. 元数据管理(数据分布地图)│  │
│  │ 2. 调度中心(数据搬来搬去)  │  │
│  │ 3. 授时中心(分配全局时间戳)│  │
│  └─────────────────────────────┘  │
└───────────────┬────────────────────┘
                │ 告诉TiDB:数据在TiKV 3号仓库
                ↓
┌─────────────────────────────────────┐
│        TiKV Server (存储层)         │ ← 有状态,存真实数据
│  ┌─────────────────────────────┐  │
│  │ Region 1 (96MB)             │  │
│  │   ┌──────────────────┐      │  │
│  │   │ 数据副本×3       │      │  │
│  │   │ 通过Raft保持一致 │      │  │
│  │   └──────────────────┘      │  │
│  │ Region 2 (96MB)             │  │
│  │   ...                       │  │
│  └─────────────────────────────┘  │
└─────────────────────────────────────┘

二、数据“变形记”:从 SQL 到 Key-Value 🎪

1. MySQL vs TiDB 的数据观

MySQL 视角

CREATE TABLE users (
    id INT PRIMARY KEY,
    name VARCHAR(100)
);
-- 数据是"表格",有行列

TiDB 视角

// 实际上,TiDB 把一切变成 Key-Value
Key:   t{表ID}_r{行ID}
Value: {列1: 值1, 列2: 值2, ...}

// 示例:表 users (ID=123),行 id=1, name='张三'
Key:   t123_r1
Value: {"id": 1, "name": "张三"}

索引也变成 KV

// 如果有索引 idx_name(name)
Key:   t{表ID}_i{索引ID}_{索引值}_{行ID}
Value: 空

// 示例:在 name 列建索引
Key:   t123_i1_张三_1
Value: ""

2. Region:数据分片的“魔法块” ✨

传统分库分表:DBA 手动分区

-- 痛苦的手工活
CREATE TABLE orders (
    id INT
) PARTITION BY RANGE (id) (
    PARTITION p0 VALUES LESS THAN (1000000),
    PARTITION p1 VALUES LESS THAN (2000000),
    -- 写到手抽筋...
);

TiDB 自动分片

一张表 → 自动切成多个 Region
每个 Region → 约 96MB 数据
每个 Region → 3 个副本(通过 Raft)
Region 分布 → PD 自动调度

Region 可视化

 users (假设 300MB 数据)
├── Region 1: [Key: t123_r1   t123_r1000000]
   ├── Leader副本: TiKV-1
   ├── Follower副本: TiKV-2
   └── Follower副本: TiKV-3
├── Region 2: [Key: t123_r1000001  t123_r2000000]
   ├── Leader副本: TiKV-2
   ├── Follower副本: TiKV-3
   └── Follower副本: TiKV-1
└── Region 3: [Key: t123_r2000001  t123_r3000000]
    ├── Leader副本: TiKV-3
    ├── Follower副本: TiKV-1
    └── Follower副本: TiKV-2

三、一条 SQL 的“奇幻漂流” 🚣

让我们跟踪一条简单查询的完整旅程:

-- 用户执行
SELECT * FROM users WHERE id = 100;

阶段1:TiDB 接收请求(计算层)

步骤1:SQL 解析​ 🧠

// 解析成抽象语法树(AST)
type SelectStmt struct {
    Fields    []*Field  // 字段列表 [*]
    Table     *Table    // 表 users
    Where     *Expr     // WHERE id = 100
    // ...
}

步骤2:查询优化​ 🎯

-- 优化器思考:
-- 方案1:全表扫描(慢,但简单)
-- 方案2:用主键索引(快!)

-- 生成执行计划
EXPLAIN SELECT * FROM users WHERE id = 100;
-- 输出:
-- IndexLookUp_10
--   ├─IndexRangeScan_8  // 用索引找行ID
--   └─TableRowIDScan_9  // 用行ID拿完整数据

步骤3:问 PD:数据在哪? ​ 🗺️

// TiDB 问 PD:"键 t123_r100 在哪个 Region?"
PD 回答:"在 Region 2,Leader 是 TiKV-2"

阶段2:TiKV 处理请求(存储层)

步骤4:Raft 共识读取​ ⚡

// TiKV-2 (Leader) 处理读取
1. 检查自己是不是最新的 Leader
2. 读取本地数据(内存或磁盘)
3. 如果读是强一致性,需要确认大多数副本
4. 返回数据

// 如果是 Follower Read(从副本读)
SELECT /*+ READ_FROM_FOLLOWER() */ * FROM users WHERE id = 100;
// 可以从最近的副本读,减少延迟

步骤5:返回数据​ 📨

{
  "id": 100,
  "name": "张三",
  "age": 25
}

阶段3:TiDB 返回结果(计算层)

步骤6:组装结果​ 🎁

// TiDB 拿到原始 KV
Key: t123_r100
Value: {"id": 100, "name": "张三", "age": 25}

// 转换成 SQL 结果集
+-----+--------+-----+
| id  | name   | age |
+-----+--------+-----+
| 100 | 张三   | 25  |
+-----+--------+-----+

步骤7:返回给客户端​ ✅

用户:"这么快?!"
TiDB:"基操,勿6~"

四、分布式事务的“时间魔法” ⏰

问题:如何在分布式系统中保证 ACID?

单机 MySQL:用锁 + 日志

分布式 TiDB:用全局时间戳​ + Percolator 模型

场景:转账 100 元

BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;  -- 在 TiKV-1
UPDATE accounts SET balance = balance + 100 WHERE id = 2;  -- 在 TiKV-2
COMMIT;

事务流程详解:

阶段1:获取全局时间戳
// TiDB 问 PD:"给我个时间戳!"
PD:"当前全局时间戳是 1631234567890"

// 这个时间戳是事务的"身份证"
start_ts = 1631234567890
阶段2:预写阶段(Prewrite)✍️
// 对每行数据:
// 1. 写锁记录(防止别人改)
Key:   t_accounts_r1_lock
Value: {primary: t_accounts_r1, start_ts: 1631234567890}

// 2. 写数据(但标记为未提交)
Key:   t_accounts_r1_1631234567890
Value: {balance: 900, 状态: 未提交}

关键技巧:选一行作为 Primary Lock

  • 账户1(id=1)作为 Primary
  • 其他行(id=2)作为 Secondary
  • Primary 提交成功,整个事务才成功
阶段3:提交阶段(Commit)✅
// 1. 获取提交时间戳
commit_ts = 1631234567891  // 比 start_ts 大

// 2. 提交 Primary
Key:   t_accounts_r1_1631234567890
Value: {balance: 900, 状态: 已提交, commit_ts: 1631234567891}

// 3. 清理锁
删除 t_accounts_r1_lock

// 4. 异步提交 Secondary
// 事务就算成功了!
阶段4:异步清理 🧹
// 后台清理未提交的数据
// 如果客户端看到 start_ts 之后但 commit_ts 之前的数据
// 会去检查 Primary 是否提交

这个模型的精髓

  1. 不用两阶段提交(2PC)协调者,避免单点
  2. Primary 决定事务生死,简化了分布式提交
  3. 时间戳全局有序,解决分布式系统的“时钟问题”

五、PD 的“智能调度”:让数据“雨露均沾” 🌧️

PD 的三大超能力:

超能力1:Region 调度(负载均衡)
// PD 监控所有 Region
if Region 太热(访问频繁) {
    分裂 Region // 96MB → 48MB + 48MB
}

if Region 太大(>144MB) {
    分裂 Region
}

if Region 太小(<24MB) && 邻居Region也小 {
    合并 Region
}

if TiKV 节点负载不均 {
    迁移 Region 从忙节点到闲节点
}
超能力2:副本管理(高可用)
// 每个 Region 3 副本,分布在不同机器
if 副本宕机 {
    PD: "紧急!Region 1 少了个副本!"
    在健康机器上补副本
}

if 新增 TiKV 节点 {
    PD: "新同学来了,分点活给它!"
    迁移部分副本到新节点
}
超能力3:热点调度(解决瓶颈)
-- 热点问题:自增主键导致新数据都写最后一个 Region
CREATE TABLE orders (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,  -- 热点!
    ...
);

-- PD 的解决方案:
-- 1. 监控到 orders 表的最后一个 Region 访问频繁
-- 2. 分裂这个 Region
-- 3. 或者建议:用 SHARD_ROW_ID_BITS
CREATE TABLE orders (
    id BIGINT AUTO_INCREMENT PRIMARY KEY
) SHARD_ROW_ID_BITS = 4;  -- 行ID打散到16个Region

六、TiDB 的“特异功能” 🦸

功能1:HTAP(混合事务/分析处理)

传统架构:MySQL(事务)+ Hadoop(分析)

  • ETL 复杂,延迟高
  • 两套系统,数据不一致

TiDB HTAP

同一份数据,两种存储引擎:
┌─────────────────┐
│ 行存 (TiKV)     │ ← 高并发事务
│ 列存 (TiFlash)  │ ← 实时分析
└─────────────────┘
-- 启用 TiFlash
ALTER TABLE orders SET TIFLASH REPLICA 1;

-- 分析查询自动路由到 TiFlash
SELECT user_id, SUM(amount) 
FROM orders  -- TiDB 自动选择列存
GROUP BY user_id;
-- 比行存快 10-100 倍!

功能2:在线 DDL(不停机改表结构)

-- MySQL:ALTER TABLE 会锁表
-- TiDB:在线,不锁表!
ALTER TABLE users ADD COLUMN age INT;
-- 可以继续读写 users 表!

原理

  1. TiDB 创建新版本的 Schema
  2. 后台慢慢迁移数据到新结构
  3. 迁移完成后切换
  4. 用户无感知

功能3:SQL 兼容性

// 你的 Spring Boot 代码一行不用改!
spring:
  datasource:
    url: jdbc:mysql://tidb:4000/test
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456
    
// MyBatis、Hibernate、JPA 都能用!

七、故障处理:当“零件”坏掉时 🔧

场景1:一个 TiKV 节点宕机

TiKV-3 宕机!
    ↓
Region 1(副本在 TiKV-1,2,3)少了个副本
    ↓
PD 检测到(心跳超时)
    ↓
选举新 Leader(在 TiKV-1 和 TiKV-2 中选)
    ↓
在 TiKV-4 上补副本
    ↓
恢复三副本,集群健康!

用户感受:查询稍微慢了点,但能继续用

场景2:PD Leader 宕机

PD-1(Leader)宕机
    ↓
PD-2 和 PD-3 检测到
    ↓
选举新 Leader(PD-2)
    ↓
新 Leader 接管工作
    ↓
用户无感知!

关键:PD 用 etcd 的 Raft 实现,自动选主

场景3:网络分区(脑裂风险)

机房A网络断掉
    ↓
TiKV-1,2 在一个分区
TiKV-3,4,5 在另一个分区
    ↓
PD 在多数派的分区(有3个节点)继续工作
    ↓
少数派分区(2个节点)停止服务
    ↓
避免数据不一致!

八、TiDB 的设计哲学 🧘

1. 存储与计算分离

计算层 (TiDB) ← 可以独立扩展
存储层 (TiKV) ← 可以独立扩展
调度层 (PD)  ← 管理两者关系

好处

  • 计算不够?加 TiDB 节点
  • 存储不够?加 TiKV 节点
  • 互不影响,像乐高积木

2. Shared-Nothing 架构

每个节点自给自足
不共享内存,不共享磁盘
通过网络通信协作

对比 Shared-Storage

  • Shared-Storage(如 AWS Aurora):存储共享,计算分离
  • Shared-Nothing(TiDB):什么都不共享,完全分布式

3. 追求在线弹性

传统扩容:停服务 → 迁移数据 → 重启 → 祈祷
TiDB 扩容:加节点 → 自动均衡 → 继续服务

九、TiDB 的“阿喀琉斯之踵” 🦶

弱点1:跨 Region 事务开销

-- 如果事务涉及多个 Region
BEGIN;
UPDATE table1 ...;  -- Region 1
UPDATE table2 ...;  -- Region 2
UPDATE table3 ...;  -- Region 3
COMMIT;
-- 网络开销比单机 MySQL 大

优化:业务设计时,相关数据尽量在同一个 Region

弱点2:二级索引回表

-- 有索引 idx_name(name)
SELECT * FROM users WHERE name = '张三';
-- 1. 用索引找到行ID
-- 2. 用行ID去主键拿完整数据
-- 比主键查询多一次查询

优化:覆盖索引

SELECT id FROM users WHERE name = '张三';
-- 只查索引,不需要回表

弱点3:小表性能

-- 10 行的小表
SELECT * FROM small_table;
-- 可能比 MySQL 慢
-- 因为要走分布式协议

优化:小表不用 TiDB,或者用 TiDB 的缓存


十、总结:TiDB 的“道”与“术” 🎯

TiDB 的“道”(核心理念):

  1. 兼容性:让用户无缝迁移
  2. 弹性:像云一样自由伸缩
  3. 强一致:分布式也要像单机一样可靠
  4. 实时 HTAP:一套系统解决所有问题

TiDB 的“术”(技术实现):

  1. 计算存储分离:各司其职,独立扩展
  2. Raft 共识:数据安全第一
  3. Percolator 事务:分布式事务的优雅解
  4. Region 分片:自动分片,自动均衡
  5. 全局时间戳:解决分布式时钟难题

最后思考:TiDB 不是要替代 MySQL,而是给 MySQL 一个“分布式升级包”。如果你的业务遇到了 MySQL 的天花板(数据量、并发量),又不想重写业务代码,TiDB 是个绝佳选择。

就像把一辆家用轿车(MySQL)改装成了变形金刚(TiDB)——平时还能买菜,关键时刻能拯救世界!🚗➡️🤖

记住:技术选型不是选最好的,是选最合适的。TiDB 适合那些“MySQL 撑不住,但不想大动干戈”的场景。如果你的业务还在用 MySQL 单机,那就好好珍惜,别急着“分布式焦虑”。等技术债务真的来了,再请 TiDB 这位“超级英雄”出马也不迟!💪