TiDB 云原生数据库深度理解:从现实难题到设计哲学

47 阅读9分钟

目标

这不是一篇名词解释,而是一份带着问题看答案的技术解读。读完它,你会明白 TiDB 为什么这样设计、解决了什么痛点、以及你能如何用好它。

开篇:你一定会遇到的三个难题

假设你是某系统的架构师。7 万多个设备,每秒钟都在往数据库里写数据,日均数据量过亿。你一定会遇到三个“鬼问题”:

  1. 写入慢:明明集群有 3 台高性能服务器,但所有写入都卡在其中一台,TPS 死活上不去。
  2. 查询也慢:一个“过去 24 小时各车间产量趋势”的报表,跑了好几分钟,还把线上业务拖得响应变慢。
  3. 机器半夜宕机:一台服务器挂了,你得爬起来手动切主库,还担心可能丢几秒钟数据。

这些问题,本质上是单机数据库架构的天花板。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。写入流程如下:

  1. 客户端将写请求发给 Leader。
  2. Leader 将变更写入本地的 Raft 日志,并并行发送给 Follower。
  3. 多数派(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 模型,核心步骤:

  1. 选一个 Primary Key(例如工单表的记录 ID),其它涉及的 Key 都是 Secondary。
  2. 第一阶段(Prewrite):把所有要写的 Key 都加上锁,并写入数据(带上事务开始时间戳 start_ts)。
  3. 第二阶段(Commit):先提交 Primary Key(清除锁,写入最终版本 commit_ts)。此时事务已经确定成功,立即返回客户端成功。然后异步清理 Secondary Keys 上的锁。

为什么能返回这么快? 因为只要 Primary Key 提交成功,事务的结果就已确定。Secondary 的清理工作可以慢慢做,不影响客户端体验。

全局时间戳(TSO):PD 提供单调递增的全局时间戳服务,为每个事务分配 start_tscommit_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 整体架构

deepseek_mermaid_20260513_f65e08.png


附录:核心组件与术语速查表

组件角色一句话职责
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 先提交,客户端不等全部完成,兼顾正确性与性能。

它不是在让你写更复杂的代码,而是让数据库替你做更复杂的事。 这就是云原生数据库的“道”。