一、从关系表到 KV 编码
理解 TiDB 架构的第一步,是搞清楚关系型数据在底层是如何存储的。
1.1 行数据的编码
在 TiDB 中,每一行数据最终都会被编码为 KV 对存储在 TiKV 中。来看一个具体例子:
CREATE TABLE users (
id BIGINT PRIMARY KEY,
name VARCHAR(64),
age INT
);
INSERT INTO users VALUES (100, 'Alice', 28);
这行数据在 TiKV 中的存储形式:
Key 编码规则: t{table_id}_r{row_id}
Value 编码: 列值按照列顺序编码
假设 table_id = 56:
Key: t56_r100
Value: ["Alice", 28] (经过编码的二进制数据)
查看表的内部 ID:
SELECT TIDB_TABLE_ID FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name = 'users';
-- 假设查询结果为 56
-- 则该表数据在 TiKV 中的 key 前缀为: t56_r
1.2 索引的编码
二级索引同样是 KV 对,编码规则略有不同:
CREATE INDEX idx_name ON users(name);
索引在 TiKV 中的存储:
Key: t{table_id}_i{index_id}_{indexed_value}
Value: {row_id} (回表用的主键)
假设 index_id = 1:
Key: t56_i1_"Alice"
Value: 100
当执行 SELECT * FROM users WHERE name = 'Alice' 时:
步骤1: 通过索引 Key (t56_i1_"Alice") 查到 Value = 100
步骤2: 用行数据 Key (t56_r100) 查到完整行数据
这就是所谓的回表操作。如果查询只需要索引列,就能直接从索引获取结果,不需要回表,这叫做覆盖索引。
1.3 联合索引的编码
CREATE INDEX idx_status_age ON users(status, age);
Key: t{table_id}_i{index_id}_{status}_{age}_{row_id}
Value: (空,因为 row_id 已经在 Key 中了)
联合索引遵循最左前缀原则:
- WHERE status = 1 → 可以用索引
- WHERE status = 1 AND age = 28 → 可以用索引
- WHERE age = 28 → 不能用索引(缺少最左列 status)
1.4 实际验证
通过以下 SQL 可以看到 TiDB 是如何编码数据的:
-- 查看表的内部信息
SELECT
TABLE_NAME,
TIDB_TABLE_ID, -- 表 ID,用于构造 Key
TIDB_ROW_ID_SHARDING_INFO
FROM information_schema.tables
WHERE table_name = 'users';
-- 查看索引信息
SHOW INDEX FROM users;
二、TiDB Server:无状态的计算层
2.1 架构定位
TiDB Server 是无状态的 SQL 计算引擎。"无状态"意味着:
- TiDB Server 不存储任何数据
- 数据全部在 TiKV 中
- 可以随时启停 TiDB Server,不影响数据
- 新增 TiDB Server 无需数据迁移
┌──────────┐
│ 应用连接 │
└────┬─────┘
│
┌────────┼────────┐
v v v
┌──────┐┌──────┐┌──────┐
│TiDB-1││TiDB-2││TiDB-3│ 三个计算节点,无状态
└──┬───┘└──┬───┘└──┬───┘
│ │ │
└───────┼───────┘
│
┌──────┴──────┐
│ TiKV │ 数据存储层
└─────────────┘
2.2 SQL 执行流程
一条 SQL 在 TiDB Server 内部的执行过程:
SQL 语句
│
v
┌─────────────────────────────────────────┐
│ Parser (解析器) │
│ SQL → AST (抽象语法树) │
│ SELECT * FROM t WHERE id = 1 │
│ → SelectStmt { Fields: *, Where: id=1 }│
└─────────────────┬───────────────────────┘
v
┌─────────────────────────────────────────┐
│ Preprocess (预处理器) │
│ 语义检查:表是否存在、列是否存在 │
│ 权限检查:用户是否有权限 │
└─────────────────┬───────────────────────┘
v
┌─────────────────────────────────────────┐
│ Optimizer (优化器) │
│ 生成执行计划:选择索引、确定 Join 方式 │
│ 逻辑优化:谓词下推、列裁剪 │
│ 物理优化:选择代价最小的物理算子 │
└─────────────────┬───────────────────────┘
v
┌─────────────────────────────────────────┐
│ Executor (执行器) │
│ 将执行计划转换为对 TiKV 的 KV 请求 │
│ 聚合结果返回给客户端 │
└─────────────────────────────────────────┘
2.3 执行计划的构成
TiDB 的执行计划由多个算子(Operator)组成,每个算子负责一个特定的操作:
EXPLAIN SELECT * FROM users WHERE age > 25 ORDER BY name LIMIT 10;
+-------------------------------+---------+-----------+----------------------+
| id | estRows | task | operator info |
+-------------------------------+---------+-----------+----------------------+
| TopN_9 | 10.00 | root | users.name, offset:0 |
| └─TableReader_16 | 10.00 | root | data:TopN_15 |
| └─TopN_15 | 10.00 | cop[tikv] | users.name, offset:0 |
| └─Selection_14 | 3333.33 | cop[tikv] | gt(users.age, 25) |
| └─TableFullScan_13 | 10000 | cop[tikv] | table:users |
+-------------------------------+---------+-----------+----------------------+
各算子含义:
| 算子 | 含义 |
|---|---|
| TableFullScan | 全表扫描 |
| Selection | 过滤条件(WHERE age > 25) |
| TopN | Top N 排序(ORDER BY ... LIMIT) |
| TableReader | 从 TiKV 读取表数据的 Reader |
task 列说明:
| task | 执行位置 |
|---|---|
| root | 在 TiDB Server 执行 |
| cop[tikv] | 下推到 TiKV 执行(协处理器) |
可以看到,TiDB 尽量将计算下推到 TiKV 执行,这样可以减少数据传输量。
三、PD:集群的大脑
3.1 PD 的核心职责
PD(Placement Driver)虽然不直接参与数据读写,但它是整个集群的核心控制组件:
┌─────────────────────────────────────────────────┐
│ PD Server │
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │
│ │ 元数据管理 │ │ 调度中心 │ │ TSO 时间戳服务 │ │
│ └──────────┘ └──────────┘ └───────────────┘ │
│ │
│ 存储集群拓扑、Region 分布、节点状态 │
│ 决定 Region 在哪里、是否需要迁移 │
│ 为分布式事务提供全局递增时间戳 │
└───────────────────────────────────────────────────┘
3.2 元数据管理
PD 维护了完整的集群元数据:
- Store 信息:每个 TiKV 节点的地址、容量、状态
- Region 信息:每个 Region 的 Key 范围、所在 TiKV 节点、副本分布
- 表到 Region 的映射:哪些表数据在哪些 Region 上
TiDB Server 启动时,会从 PD 拉取完整的元数据缓存在本地。之后 TiDB Server 可以直接根据元数据定位数据所在 Region,并与 TiKV 直接通信,不再经过 PD。
3.3 调度中心
PD 持续监控集群状态,并在以下情况下触发调度:
| 触发条件 | 调度动作 |
|---|---|
| 新增/下线 TiKV 节点 | 迁移 Region 到新/其他节点 |
| Region 分布不均 | 将 Region 从高负载节点迁走 |
| Region 过大 | 分裂 Region(Split) |
| 热点 Region | 分裂热点 Region,分散负载 |
| 副本数不足 | 为新 Region 创建副本 |
调度是后台自动进行的,不影响业务正常运行。
3.4 TSO(全局时间戳服务)
TSO 是 PD 提供的全局递增时间戳服务,是 TiDB 实现分布式事务的基础:
TiDB-1 ─── 获取 TSO ──> PD
│
v
返回: 428888888888888321
(全局唯一递增时间戳)
TiDB-2 ─── 获取 TSO ──> PD
│
v
返回: 428888888888888322
(比上一个更大)
这个全局时间戳确保了:
- 分布式事务的顺序性
- 快照隔离的一致性
- 跨节点操作的可串行化
3.5 PD 的高可用
PD 基于 Raft 协议实现高可用,典型部署为 3 或 5 个节点:
PD-1 (Leader) ← 处理所有读写请求
PD-2 (Follower) ← 同步数据,可接替 Leader
PD-3 (Follower) ← 同步数据,可接替 Leader
当 Leader 故障时,剩余节点通过 Raft 选举产生新 Leader。只要多数派节点存活,PD 就能正常工作。
四、TiKV:分布式事务存储引擎
4.1 Region — 数据分布的基本单位
TiKV 中的数据不是整块存放的,而是被切分为一个个 Region:
Key 空间
│
├─── Region A: [Key 0001, Key 5000]
│ ├─ Replica 1 → TiKV Node 1 (Leader)
│ ├─ Replica 2 → TiKV Node 2 (Follower)
│ └─ Replica 3 → TiKV Node 3 (Follower)
│
├─── Region B: [Key 5001, Key 10000]
│ ├─ Replica 1 → TiKV Node 2 (Leader)
│ ├─ Replica 2 → TiKV Node 3 (Follower)
│ └─ Replica 3 → TiKV Node 1 (Follower)
│
└─── Region C: [Key 10001, Key 15000]
...
每个 Region:
- 默认大小 96 MB,超过后自动分裂
- 有 3 个副本(默认),通过 Raft 保持一致
- 每个副本有一个 Leader,负责处理读写;Follower 只同步数据
4.2 Raft 协议简述
Raft 是一种分布式一致性协议,确保多个副本的数据一致:
写请求到达 Region Leader
│
v
Leader 将写入写入本地 WAL
│
v
Leader 将数据发给 Follower
│
v
收到多数派 (2/3) 确认后提交
│
v
返回成功给客户端
关键点:只要多数派(3 副本中的 2 个)存活,数据就不会丢失,服务就不会中断。
4.3 TiKV 的存储引擎
TiKV 底层使用 RocksDB 作为存储引擎,其数据结构为 LSM-Tree:
写请求
│
v
MemTable (内存中的跳表) ← 写入先到内存
│
v 当 MemTable 满时
SST File (磁盘上的有序文件)
│
v 多层合并
L0 → L1 → L2 → ... → Ln
LSM-Tree 的特点:
- 写入性能高:追加写,不需要随机写
- 读取需要合并:数据可能分布在多层,需要合并查找
- 适合写多读少的场景
为了加速读取,TiKV 使用了 Block Cache 和 RocksDB 的 Bloom Filter:
读取 Key
│
v
Block Cache 中查找
│ 未命中
v
Bloom Filter 判断"一定不存在"还是"可能存在"
│ 可能存在
v
从 L0 → L1 → ... 逐层查找
4.4 MVCC(多版本并发控制)
TiKV 使用 MVCC 来支持并发读写的一致性:
同一行数据可能有多个版本:
Key: t56_r100
Version 1 (TSO=100): value = "Alice, age 28"
Version 2 (TSO=200): value = "Alice, age 29"
Version 3 (TSO=300): value = "Alice, age 30"
每个事务获取数据时,根据事务开始时的 TSO 确定能看到哪个版本:
事务 A (start_ts=150): 看到 Version 1 (Alice, age 28)
事务 B (start_ts=250): 看到 Version 2 (Alice, age 29)
事务 C (start_ts=350): 看到 Version 3 (Alice, age 30)
这样不同事务之间不会互相阻塞,提高了并发性能。
4.5 两阶段提交(2 PC)
TiDB 的分布式事务使用 Percolator 模型,其核心是两阶段提交:
事务:UPDATE users SET age = 30 WHERE id = 100
第一阶段:Prewrite
┌─────────────────────────────────────────┐
│ 1. 选择一行作为 Primary Lock │
│ 其他行为 Secondary Lock │
│ 2. 将 Lock 写入所有涉及的 Key │
│ 3. 将实际数据写入(带 Lock 标记) │
│ 4. 先写 Primary,再写 Secondary │
└────────────────┬────────────────────────┘
v
第二阶段:Commit
┌─────────────────────────────────────────┐
│ 1. 向 PD 获取 commit_ts │
│ 2. 提交 Primary Lock │
│ 3. 异步提交 Secondary Lock │
│ 4. 返回成功给客户端 │
└─────────────────────────────────────────┘
为什么先提交 Primary?
只要 Primary 提交成功,事务就成功了。Secondary 可以异步完成,即使 Secondary 提交失败,后续读取也可以通过 Resolve Lock 机制帮助完成提交。
五、组件间的协作
5.1 完整的读写流程
写入流程
┌───────────────────────────────────────────┐
│ │
│ 客户端 │
│ │ │
│ v │
│ TiDB Server │
│ │ 1. SQL 解析 → 执行计划 │
│ │ 2. 获取 commit_ts (TSO) │
│ │ 3. Prewrite: 写入 Lock │
│ │ → 直接发往 TiKV Region Leader │
│ │ 4. Commit: 提交事务 │
│ │ → 先提交 Primary,再异步 Secondary │
│ v │
│ TiKV │
│ │ 1. 接收 KV 请求 │
│ │ 2. 写入 MemTable/WAL │
│ │ 3. Raft 复制到 Follower │
│ │ 4. 返回确认 │
│ v │
│ PD (仅在获取 TSO 和元数据时参与) │
│ │
└───────────────────────────────────────────┘
读取流程
┌───────────────────────────────────────────┐
│ │
│ 客户端 │
│ │ │
│ v │
│ TiDB Server │
│ │ 1. SQL 解析 → 确定访问哪些 Region │
│ │ 2. 构造 KV 请求 │
│ │ 3. 直接发送给对应 TiKV Region Leader │
│ │ 4. 聚合结果返回客户端 │
│ v │
│ TiKV │
│ │ 1. 接收 KV 读取请求 │
│ │ 2. 根据 MVCC 找到对应版本 │
│ │ 3. 返回数据 │
│ │
└───────────────────────────────────────────┘
5.2 元数据更新流程
当表结构变化时(如 DDL),元数据如何传播:
DDL: ALTER TABLE users ADD COLUMN phone VARCHAR(20);
1. TiDB Server 执行 DDL
│
v
2. 将 DDL 变更写入 TiKV(通过 distributed DDL 协议)
│
v
3. PD 感知到元数据变更
│
v
4. PD 通知所有 TiDB Server 更新缓存
│
v
5. TiDB Server 加载最新元数据
│
v
6. DDL 完成,客户端可以继续操作
在线 DDL 不会阻塞 DML(数据读写),这是因为 TiDB 使用了类似 gh-ost 的在线 schema 变更算法。
六、查看集群状态实践
6.1 通过 SQL 查看集群信息
-- 查看各组件信息
SELECT * FROM information_schema.cluster_info;
-- 输出包含组件类型、版本、地址、启动时间等
+------+-----------------+-----------------+---------+------------------------------------------+---------------------+------------------+-----------+
| TYPE | INSTANCE | STATUS_ADDRESS | VERSION | GIT_HASH | START_TIME | UPTIME | SERVER_ID |
+------+-----------------+-----------------+---------+------------------------------------------+---------------------+------------------+-----------+
| tidb | 127.0.0.1:4000 | 127.0.0.1:10080 | 8.5.0 | d13e52ed6e22cc5789bed7c64c861578cd2ed55b | 2026-05-21 16:36:45 | 40m52.372333026s | 1385 |
| tidb | 127.0.0.1:4001 | 127.0.0.1:10081 | 8.5.0 | d13e52ed6e22cc5789bed7c64c861578cd2ed55b | 2026-05-21 16:36:45 | 40m52.372340986s | 640 |
| pd | 127.0.0.1:2382 | 127.0.0.1:2382 | 8.5.0 | d190c0e9082de46128b756f93b1291768dda645a | 2026-05-21 16:36:06 | 41m31.372343796s | 0 |
| pd | 127.0.0.1:2379 | 127.0.0.1:2379 | 8.5.0 | d190c0e9082de46128b756f93b1291768dda645a | 2026-05-21 16:36:06 | 41m31.372349536s | 0 |
| pd | 127.0.0.1:2384 | 127.0.0.1:2384 | 8.5.0 | d190c0e9082de46128b756f93b1291768dda645a | 2026-05-21 16:36:06 | 41m31.372351846s | 0 |
| tikv | 127.0.0.1:20160 | 127.0.0.1:20180 | 8.5.0 | a2c58c94f89cbb410e66d8f85c236308d6fc64f0 | 2026-05-21 16:36:21 | 41m16.372354186s | 0 |
| tikv | 127.0.0.1:20161 | 127.0.0.1:20181 | 8.5.0 | a2c58c94f89cbb410e66d8f85c236308d6fc64f0 | 2026-05-21 16:36:21 | 41m16.372358676s | 0 |
| tikv | 127.0.0.1:20162 | 127.0.0.1:20182 | 8.5.0 | a2c58c94f89cbb410e66d8f85c236308d6fc64f0 | 2026-05-21 16:36:21 | 41m16.372360926s | 0 |
+------+-----------------+-----------------+---------+------------------------------------------+---------------------+------------------+-----------+
-- 查看 TiKV Store 状态
SELECT
STORE_ID,
ADDRESS,
STORE_STATE,
CAPACITY,
AVAILABLE,
LEADER_COUNT,
REGION_COUNT
FROM information_schema.tikv_store_status;
+----------+-----------------+-------------+----------+-----------+--------------+--------------+
| STORE_ID | ADDRESS | STORE_STATE | CAPACITY | AVAILABLE | LEADER_COUNT | REGION_COUNT |
+----------+-----------------+-------------+----------+-----------+--------------+--------------+
| 5 | 127.0.0.1:20162 | 0 | 925GiB | 843.3GiB | 21 | 79 |
| 1 | 127.0.0.1:20160 | 0 | 925GiB | 843.3GiB | 28 | 79 |
| 4 | 127.0.0.1:20161 | 0 | 925GiB | 843.3GiB | 30 | 79 |
+----------+-----------------+-------------+----------+-----------+--------------+--------------+
-- 查看 Region 分布
SELECT
REGION_ID,
START_KEY,
END_KEY,
APPROXIMATE_SIZE,
APPROXIMATE_KEYS
FROM information_schema.tikv_region_status
LIMIT 10;
6.2 通过 PD API 查看
# 查看集群状态
curl http://127.0.0.1:2379/pd/api/v1/health
# 查看 Store 信息
curl http://127.0.0.1:2379/pd/api/v1/stores
# 查看 Region 统计
curl http://127.0.0.1:2379/pd/api/v1/regions
# 查看集群配置
curl http://127.0.0.1:2379/pd/api/v1/config
6.3 通过 TiDB Dashboard 查看
访问 http://<tidb-server>:2379/dashboard 可以打开 TiDB Dashboard,提供:
- 集群概况(QPS、延迟、存储使用率)
- Top SQL 分析
- 慢查询分析
- 可视化流量
- 集群诊断
- 日志搜索
七、理解数据分布与热点
7.1 什么是写入热点
当大量写入集中在某个 Region 时,该 Region 所在的 TiKV 节点会成为性能瓶颈:
TiKV Node 1 ━━━━━ Region A (热点) ← 大量写入打到这里
TiKV Node 2 ━━━━━ Region B ← 空闲
TiKV Node 3 ━━━━━ Region C ← 空闲
7.2 热点产生的原因
最常见的原因是单调递增的主键:
CREATE TABLE logs (
id BIGINT AUTO_INCREMENT PRIMARY KEY, -- 连续递增
message TEXT
);
由于数据按 Key 范围分布到 Region,连续递增的主键会让所有新数据都写入最后一个 Region:
Region A: [1, 1000] → 不再写入
Region B: [1001, 2000] → 不再写入
Region C: [2001, 3000] → 所有新写入都集中在这里!热点!
7.3 如何避免热点
方案一:使用 AUTO_RANDOM(推荐)
CREATE TABLE logs (
id BIGINT PRIMARY KEY AUTO_RANDOM, -- 自动打散
message TEXT
);
AUTO_RANDOM 将自增值编码后分散到整个 Key 空间,写入自然分散到不同 Region。
方案二:Shard Row ID Bits
CREATE TABLE logs (
id BIGINT AUTO_INCREMENT,
message TEXT
) SHARD_ROW_ID_BITS = 4 PRE_SPLIT_REGIONS = 4;
这种方式会打散内部 Row ID 的分布。
方案三:业务层面打散
-- 使用 UUID 或雪花 ID 作为主键
-- 在应用层生成全局唯一 ID
八、小结
深入解析了 TiDB 的三大核心组件:
- TiDB Server:无状态计算层,负责 SQL 解析、优化和执行
- PD Server:集群大脑,管理元数据、调度、分配全局时间戳
- TiKV Server:分布式存储引擎,基于 Region + Raft + MVCC 实现高可用