目标
这不是一篇名词解释,而是一份带着问题看答案的技术解读。读完它,你会明白 TiDB 为什么这样设计、解决了什么痛点、以及你能如何用好它。
开篇:你一定会遇到的三个难题
假设你是某系统的架构师。7 万多个设备,每秒钟都在往数据库里写数据,日均数据量过亿。你一定会遇到三个“鬼问题”:
- 写入慢:明明集群有 3 台高性能服务器,但所有写入都卡在其中一台,TPS 死活上不去。
- 查询也慢:一个“过去 24 小时各车间产量趋势”的报表,跑了好几分钟,还把线上业务拖得响应变慢。
- 机器半夜宕机:一台服务器挂了,你得爬起来手动切主库,还担心可能丢几秒钟数据。
这些问题,本质上是单机数据库架构的天花板。TiDB 这类云原生数据库,就是为破解这三个难题而生的。
第一部分:写入慢?让所有机器一起干活
1.1 问题根因:你把数据都塞进了同一个抽屉
传统 MySQL 主从架构中,写入只能由主库处理。即使你用了分库分表中间件,也得业务代码决定数据往哪个分片写 —— 繁琐且容易出错。
TiDB 的思路截然不同:让数据库自己决定数据放哪,并且让所有节点都能写。
这就需要解决一个核心问题:数据怎么打散?
1.2 TiDB 的解法:Region + 合理的分片键
TiDB 把数据切成一个个 Region(你可以理解为 96MB 大小的“数据抽屉”)。每个抽屉只放在一台机器上。写入时,数据属于哪个抽屉,就由对应的机器处理。
关键在于:如果抽屉分配得好,所有机器都能同时写入。
你犯过的错误示例:使用 collect_time 作为主键前缀,导致新数据全部写入最后一个抽屉 → 一台机器写,其他机器闲着 → TPS 只有 1200。
-- ❌ 错误:按时间戳做主键前缀,所有写入挤在最后一个 Region
CREATE TABLE device_data (
collect_time DATETIME,
device_id VARCHAR(32),
value DECIMAL(18,4),
PRIMARY KEY (collect_time, device_id)
);
正确的做法:让数据均匀分布到所有抽屉。两种常用方法:
| 方法 | 原理 | 优点 | 缺点 |
|---|---|---|---|
AUTO_RANDOM 主键 | 主键值随机分布 | 写入均匀,简单 | 查询需二级索引+回表 |
| 哈希分区表 | 按设备 ID 哈希分区 | 查询无需回表 | 查询必须带分区键 |
在你的 IoT 场景中,推荐哈希分区表:以 device_id 为分区键,数据自动散到 128 个抽屉,每个抽屉有自己的机器处理写入。
-- ✅ 正确:哈希分区,写入均匀分散
CREATE TABLE device_data (
device_id VARCHAR(32),
collect_date DATE,
collect_time DATETIME,
value DECIMAL(18,4),
PRIMARY KEY (device_id, collect_date, collect_time)
) PARTITION BY HASH(device_id) PARTITIONS 128;
优化后,写入 TPS 从 1200 提升到 5200。
核心原则:写入性能的关键,不是机器有多强,而是负载有没有均匀摊到所有机器上。
第二部分:查询也慢?让行存和列存各司其职
2.1 问题根因:行存天生不适合做统计
传统数据库按行存储:一行数据的所有字段挨着放。执行 SELECT AVG(temperature) FROM device_data 时,数据库必须把整行的所有列都读出来,再在内存中抽取 temperature 列——即使你只需要这一列。
这就是行存的痛点:读入的数据中,90% 都是当前查询不需要的。
2.2 TiDB 的解法:再加一份列存副本
TiDB 的做法很巧妙:不改变原有的行存,而是额外维护一份列存副本——这就是 TiFlash。
- 行存(TiKV):负责高频写入和点查,数据按行组织。
- 列存(TiFlash):负责复杂分析查询,把同一列的数据连续存放。查询
AVG(temperature)时只读这一列,I/O 量只有行存的几十分之一,聚合速度可提升 10 倍以上。
| 查询类型 | TiKV 行存表现 | TiFlash 列存表现 |
|---|---|---|
点查 WHERE id = 100 | ✅ 极快 | ⚠️ 稍慢 |
聚合 AVG(value) | ❌ 全表扫描,I/O 大 | ✅ 只读单列,极快 |
| 范围更新 | ✅ 支持 | ❌ 不支持写入 |
2.3 关键问题:列存副本如何同步?
你不可能让业务代码写两遍数据(写 TiKV + 写 TiFlash)。TiDB 用了一个聪明的设计:Raft Learner。
- 每个 TiKV Region 有一个 Leader 处理写入,若干个 Follower 同步日志。
- TiFlash 作为一个 Learner(观察者),异步接收 Leader 的 Raft 日志,自己转换成列存格式并落盘。
- 最重要的特性:Learner 不参与 Leader 选举,也不参与日志提交的多数派确认。因此,即使 TiFlash 慢了或暂时宕机,也完全不影响 TiKV 的写入性能和事务提交。
这就是读写物理隔离:写入走行存通道,分析查询走列存通道,互不干扰。
flowchart LR
Leader[TiKV Leader] -->|Raft Log| Follower1[TiKV Follower]
Leader -->|Raft Log| Follower2[TiKV Follower]
Leader -.->|异步推送| Learner[TiFlash Learner]
Learner --> Convert[转换为列存]
Convert --> ColumnFile[列存文件]
核心原则:没有一种存储格式能通吃所有场景。TiDB 的选择是:小孩子才做选择,我全都要——行存和列存各存一份,自动同步。
第三部分:机器宕机怎么办?自动恢复,你不用半夜爬起来
3.1 问题根因:传统主从切换要么慢,要么丢数据
MySQL 主从架构中,如果主库宕机:
- 手动切换:你被叫醒,查状态,切 VIP,少则 10 分钟。
- MHA 自动切换:虽然快一点,但异步复制下仍可能丢几秒数据。
3.2 TiDB 的解法:Raft 协议 + PD 调度
Raft 协议:每个 Region 有 3 个副本,其中一个是 Leader,另外两个是 Follower。写入流程如下:
- 客户端将写请求发给 Leader。
- Leader 将变更写入本地的 Raft 日志,并并行发送给 Follower。
- 当多数派(3 个副本中的 2 个)确认收到日志后,Leader 才将该日志应用到状态机(真正写入),然后返回客户端成功。
这个机制带来的好处:
- 数据零丢失(RPO = 0):因为只有多数派确认才算成功,少数副本宕机不影响已提交数据。
- 自动故障转移(RTO ≤ 30 秒):如果 Leader 宕机,剩下的 Follower 会自动选举出新的 Leader,业务无感知。
PD 的职责:PD 是集群的“调度员”,它通过心跳感知每个 TiKV 节点的健康状况。
- 发现节点宕机 → 将其上的 Region 副本调度到其他健康节点,补足副本数。
- 发现某个节点负载过高(例如承担了太多 Region Leader)→ 主动将部分 Leader 迁移到其他节点,实现负载均衡。
核心原则:高可用的核心不是“不坏”,而是“坏了能自动修,且用户无感知”。Raft 保证了数据不丢,PD 保证了副本不缺。
第四部分:跨节点写入如何保持原子性?分布式事务的优雅实现
4.1 问题根因:一笔事务涉及多个 Region
如果一笔事务要同时更新工单表和库存表,而这两张表的数据位于不同的 Region(甚至不同的 TiKV 节点),怎么保证要么全成功,要么全失败?
4.2 TiDB 的解法:Percolator 模型(优化版两阶段提交)
TiDB 的分布式事务基于 Google 的 Percolator 模型,核心步骤:
- 选一个 Primary Key(例如工单表的记录 ID),其它涉及的 Key 都是 Secondary。
- 第一阶段(Prewrite):把所有要写的 Key 都加上锁,并写入数据(带上事务开始时间戳
start_ts)。 - 第二阶段(Commit):先提交 Primary Key(清除锁,写入最终版本
commit_ts)。此时事务已经确定成功,立即返回客户端成功。然后异步清理 Secondary Keys 上的锁。
为什么能返回这么快? 因为只要 Primary Key 提交成功,事务的结果就已确定。Secondary 的清理工作可以慢慢做,不影响客户端体验。
全局时间戳(TSO):PD 提供单调递增的全局时间戳服务,为每个事务分配 start_ts 和 commit_ts,保证了事务的先后顺序和快照隔离级别。
sequenceDiagram
participant Client
participant TiDB
participant PD
participant TiKV_A as TiKV (Primary)
participant TiKV_B as TiKV (Secondary)
Client->>TiDB: BEGIN
TiDB->>PD: 获取 start_ts
PD-->>TiDB: start_ts = 100
TiDB-->>Client: OK
Client->>TiDB: COMMIT
TiDB->>PD: 获取 commit_ts
PD-->>TiDB: commit_ts = 110
TiDB->>TiKV_A: Prewrite (Primary, data, start_ts=100)
TiKV_A-->>TiDB: 加锁成功
TiDB->>TiKV_B: Prewrite (Secondary, data, start_ts=100)
TiKV_B-->>TiDB: 加锁成功
TiDB->>TiKV_A: Commit (Primary, commit_ts=110)
TiKV_A-->>TiDB: 成功
TiDB-->>Client: 事务提交成功
TiDB->>TiKV_B: Commit (Secondary, commit_ts=110) # 异步
核心原则:分布式事务的难点不是“怎么写”,而是“怎么快”。Percolator 模型把最关键的决策点(Primary)提前提交,让客户端不用等所有节点都完成。
一张图记住 TiDB 整体架构
附录:核心组件与术语速查表
| 组件 | 角色 | 一句话职责 |
|---|---|---|
| TiDB Server | 计算层 | 无状态 SQL 引擎,解析 SQL 并生成执行计划。 |
| TiKV | 行存储层 | 分布式 KV 存储,按行存数据,通过 Raft 保证一致性。 |
| TiFlash | 列存储层 | 列存副本,异步同步 TiKV,加速 OLAP 查询。 |
| PD | 管控层 | 元数据管理、TSO 时间戳、Region 调度、健康检测。 |
| Region | 逻辑分片 | 数据分布的最小单元,约 96 MB。 |
| SST | 物理文件 | RocksDB 的持久化文件,一个 SST 可包含多个 Region 数据。 |
| MANIFEST | 元数据文件 | RocksDB 的“总账本”,记录 SST 文件与 Region 的映射。 |
| Raft | 共识算法 | 负责 TiKV 内副本间数据一致性与 Leader 选举。 |
| Learner | 特殊副本 | 不参与选举和多数派确认,用于 TiFlash 同步。 |
| TSO | 时间戳 | PD 分配的全局单调递增时间戳,支撑分布式事务。 |
写在最后:TiDB 的设计哲学
TiDB 所做的一切,无非是不断识别系统的瓶颈点,然后用分布式的手段去解决——并且尽量让这些复杂性对业务代码透明。
- 写入慢 → 把数据打散到所有抽屉(Region),让所有机器一起写。
- 查询慢 → 加一份列存副本(TiFlash),让分析走快车道,且不影响写入。
- 机器宕机 → Raft 保证数据不丢,PD 保证副本不缺,故障自动恢复。
- 跨节点事务 → Primary 先提交,客户端不等全部完成,兼顾正确性与性能。
它不是在让你写更复杂的代码,而是让数据库替你做更复杂的事。 这就是云原生数据库的“道”。