1. 从一次深夜告警说起:TiDB到底解决了什么问题
凌晨两点,手机突然响了。监控平台推来一条告警:某核心业务库的写入延迟飙到 800ms,主从延迟突破 30 秒,下游报表系统已经开始读到脏数据。我爬起来连上服务器,一看慢查询日志——单表数据量刚过 8000 万行,几个大报表的聚合查询把 IO 打满了。这种场景做过几年后端的人应该都不陌生:业务在涨,数据在涨,但底层还是那个单机 MySQL,垂直扩容已经顶到天花板,分库分表改造成本高得吓人,而且一旦分片键选错,后面想改几乎等于重做。
那段时间我就在想,有没有一种数据库,既能让开发像用单机 MySQL 一样写 SQL,又能在底层自动搞定水平扩展、强一致和高可用?后来接触到了 TiDB,才算真正把这个问题想明白。TiDB 是一个开源的分布式 SQL 数据库,定位是NewSQL——这个词拆开看就是"New + SQL",意思是它保留了传统关系型数据库的 SQL 接口和事务能力,同时吸收了分布式系统的水平扩展和容错特性。你可以把它理解成"一个会自己长大的 MySQL":对外,你连上去写CREATE TABLE、INSERT、JOIN,语法和 MySQL 高度兼容;对内,它把数据切成无数小块,分散在几十上百个节点上,任何一个节点挂了都不影响整体服务。
这篇文章适合谁看?如果你是被单库容量逼到墙角的后端开发,是正在做技术选型的架构师,或者只是听说过 TiDB 但一直没搞懂它和 MySQL、HBase、分库分表中间件到底差在哪,那这篇内容应该能帮你把脉络理清楚。我不会只讲概念,会把它的核心组件、数据怎么切、事务怎么保证、实际部署时踩过的坑,都摊开来说。看完你至少能判断:自己的业务到底该不该上 TiDB,以及如果上,第一步该干什么。
2. 拆开TiDB看本质:四个核心组件各干什么活
2.1 计算与存储分离:TiDB Server和TiKV的分工逻辑
TiDB 最核心的设计思想就四个字:计算存储分离。这个思路其实不新鲜,很多分布式系统都这么干,但 TiDB 把它做得比较彻底。整个集群里,负责"算"的是 TiDB Server,负责"存"的是 TiKV,两者之间通过一个叫 PD 的调度组件来协调。
TiDB Server 本身是无状态的。什么叫无状态?就是它不存任何业务数据,只负责接收客户端发来的 SQL,做语法解析、生成执行计划、然后去 TiKV 拉数据、算结果、返回给客户端。你可以把它当成一个"SQL 翻译官 + 计算引擎"。因为无状态,所以你可以随便起多个 TiDB Server,前面挂个负载均衡,请求打到哪个都行,加机器就能提升并发处理能力。这一点和传统 MySQL 主从完全不一样——MySQL 的从库是只读的,写还得找主库,而 TiDB Server 每个节点都能读写。
TiKV 才是真正存数据的地方。它是一个分布式 Key-Value 存储引擎,数据按范围切成一个个 Region,每个 Region 默认 96MB 左右,分散在不同 TiKV 节点上。TiKV 用的是RocksDB作为单机存储引擎,RocksDB 又是基于 LSM-Tree 的,写性能很好,适合高吞吐写入场景。这里有个关键点:TiKV 不是简单地把数据存进去就完事,它用Raft 协议来保证每个 Region 有多副本,通常默认三副本。也就是说,同一份数据会在三个不同的 TiKV 节点上各存一份,任何一个节点挂了,Raft 会自动选主,服务不中断。
提示:TiDB Server 无状态这个特性非常关键。意味着你可以随时扩缩容计算层,业务高峰期加两个节点扛流量,低谷期再撤掉,成本可控。但 TiKV 是有状态的,扩缩容会触发数据搬迁,需要谨慎操作。
2.2 PD:集群的大脑,负责调度和分配
PD 全称 Placement Driver,是整个 TiDB 集群的"大脑"。它不存业务数据,但存了所有数据的元信息:每个 Region 在哪个 TiKV 上、哪个是 Leader、集群有哪些节点、每个节点的负载情况等等。PD 主要干三件事:
第一,分配全局唯一的时间戳(TSO)。TiDB 的事务用的是全局单调递增的时间戳来做版本控制,这个时间戳就是 PD 统一分配的。所有事务开始和提交都要找 PD 要时间戳,所以 PD 的性能直接影响整个集群的事务吞吐。生产环境一般会部署至少三个 PD 节点组成集群,避免单点故障。
第二,调度 Region。PD 会持续监控每个 TiKV 节点的数据量、读写热点,如果发现某个节点数据太多或者太热,就会把一部分 Region 调度到空闲节点上,实现负载均衡。这个调度是自动的,不需要人工干预。
第三,维护集群拓扑。新节点加入、旧节点下线,PD 都能感知并触发相应的数据搬迁。比如你新加了一个 TiKV 节点,PD 会慢慢把其他节点上的 Region 挪一部分过来,最终达到均衡。
2.3 TiFlash:列存副本,让分析查询不再拖垮在线业务
TiDB 早期只有行存,做 OLTP 没问题,但跑复杂分析查询(比如全表扫描、大聚合)就很吃力,因为行存要读大量不需要的列。后来引入了TiFlash,本质是一个列式存储副本。你可以在建表时指定某些表同步一份列存副本到 TiFlash,TiDB 的优化器会自动判断:简单点查走 TiKV 行存,复杂分析走 TiFlash 列存。
这个设计的好处是HTAP(Hybrid Transactional/Analytical Processing),也就是一套系统同时扛在线事务和分析查询,不用再单独搭一套数仓做 ETL 同步。TiFlash 的数据同步是通过 Raft Learner 机制从 TiKV 异步复制的,不阻塞在线写入。当然,异步意味着有微小延迟,对实时性要求极高的分析场景要留意这一点。
2.4 组件协作全景:一条SQL的完整旅程
假设你连上 TiDB Server 发了一条SELECT * FROM orders WHERE user_id = 1001,背后发生了什么?
- TiDB Server 收到 SQL,解析成抽象语法树,生成逻辑执行计划,再优化成物理执行计划。
- 优化器发现
user_id上有索引,决定走索引扫描。 - TiDB Server 向 PD 请求这次查询需要的数据所在 Region 的位置信息(如果缓存里没有的话)。
- 根据 PD 返回的地址,TiDB Server 直接向对应的 TiKV 节点发起 RPC 调用,读取数据。
- TiKV 收到请求,从 RocksDB 里读出对应的 Key-Value,返回给 TiDB Server。
- TiDB Server 做必要的过滤、聚合、排序,把最终结果返回给客户端。
整个过程对客户端完全透明,你感觉就是在用一个单机数据库。但底层可能涉及几十个节点的协作。这就是 TiDB 最值钱的地方:把分布式复杂度留给自己,把简单留给用户。
3. 数据怎么切、事务怎么保:TiDB的核心机制拆解
3.1 Region切分与调度:数据自动均衡的秘密
TiDB 把整个 Key-Value 空间按顺序切成一段段,每一段就是一个Region。刚开始只有一个 Region,随着数据写入,Region 会分裂成两个,然后继续分裂,最终形成很多个 Region 分散在不同 TiKV 上。每个 Region 默认大小 96MB,这个值可以通过配置调整,但一般不建议改太小,否则 Region 数量爆炸,PD 的调度压力会很大。
Region 的副本组用 Raft 协议维护,一个 Region 的三个副本组成一个 Raft Group,其中一个 Leader 负责读写,另外两个 Follower 负责同步数据。当 Leader 所在节点故障,Raft 会在秒级内重新选主,业务几乎无感知。这里有个细节:TiDB 的 Raft 实现做了Multi-Raft优化,也就是一个 TiKV 节点上可能同时承载几百上千个 Region 的副本,每个 Region 独立跑 Raft,互不影响。这样单个节点的故障只会影响它上面的那些 Region,而且这些 Region 会迅速在其他节点上恢复。
PD 的调度策略有几个关键参数:max-snapshot-count控制同时进行快照复制的 Region 数量,max-pending-peer-count控制等待调度的副本数,leader-schedule-limit控制 Leader 调度的并发度。生产环境如果发现调度太慢或者太快影响性能,可以调这些参数。我一般建议默认值先跑,观察一段时间再按需微调。
3.2 分布式事务:乐观锁与悲观锁怎么选
TiDB 支持完整的 ACID 事务,底层用的是Percolator 模型,这是 Google 在 2010 年发的一篇论文里提出的分布式事务方案。简单说,它用一个全局时间戳来给事务排序,每个 Key 的每个版本都带一个时间戳,读的时候读小于当前时间戳的最新版本,写的时候先写一个"锁"标记,提交时再真正写入并清除锁。
TiDB 提供两种事务模式:乐观事务和悲观事务。乐观事务是早期默认模式,适合冲突少的场景,提交时才检测冲突,冲突了就回滚重试。悲观事务是后来加的,更接近 MySQL 的行为,在事务执行过程中就加锁,冲突时等待而不是直接失败。对于从 MySQL 迁移过来的业务,我强烈建议用悲观事务,因为很多业务代码没做重试逻辑,乐观事务一旦冲突就报错,业务层会炸。
-- 设置悲观事务模式(会话级别) SET SESSION tidb_txn_mode = 'pessimistic'; -- 或者全局设置 SET GLOBAL tidb_txn_mode = 'pessimistic';事务隔离级别方面,TiDB 默认是Snapshot Isolation,也支持Read Committed。Snapshot Isolation 能避免脏读和不可重复读,但可能出现写偏斜(Write Skew)。如果业务对一致性要求极高,可以用SELECT ... FOR UPDATE加锁,或者把隔离级别调到 Read Committed 配合悲观锁。
注意:TiDB 的单事务大小有限制,默认
txn-total-size-limit是 100MB。如果你有一个超大事务,比如批量导入几百万行,可能会超限报错。解决办法是拆成多个小事务,或者用LOAD DATA这种绕过事务的批量导入方式。
3.3 索引与查询优化:为什么你的SQL在TiDB上变慢了
TiDB 的索引和 MySQL 类似,支持主键索引、二级索引、联合索引、唯一索引。但有几个差异点需要特别注意:
第一,TiDB 的主键默认是聚簇索引(从 5.0 开始支持非聚簇,但默认还是聚簇)。这意味着主键值直接决定数据在 TiKV 上的物理分布。如果主键是自增 ID,写入会集中在最后一个 Region,形成热点。解决办法是用AUTO_RANDOM或者SHARD_ROW_ID_BITS打散写入。
-- 使用 AUTO_RANDOM 打散主键 CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_RANDOM, user_id BIGINT, amount DECIMAL(10,2), KEY idx_user (user_id) );第二,TiDB 的优化器是基于代价的(CBO),和 MySQL 的优化器不是一回事。有些在 MySQL 上跑得很快的 SQL,在 TiDB 上可能选错执行计划。这时候可以用EXPLAIN看执行计划,用/*+ HINT */强制走某个索引或某个 Join 算法。
-- 查看执行计划 EXPLAIN SELECT * FROM orders WHERE user_id = 1001; -- 强制使用索引 SELECT /*+ USE_INDEX(orders, idx_user) */ * FROM orders WHERE user_id = 1001;第三,统计信息很重要。TiDB 靠统计信息来估算行数和选择执行计划,如果统计信息过期,优化器可能做出错误判断。可以手动触发统计信息收集:
ANALYZE TABLE orders;我踩过的一个坑:有一张表数据量很大,但统计信息很久没更新,优化器以为只有几千行,结果选了个 Nested Loop Join,实际跑起来几十分钟没出结果。后来手动 ANALYZE 之后,优化器改成了 Hash Join,几秒钟就出来了。所以生产环境一定要配置自动统计信息收集,或者定期手动跑。
4. 从零搭一套TiDB:实操部署与关键配置
4.1 环境规划:节点数量与硬件选型
部署 TiDB 之前,先想清楚要几个节点、什么配置。最小生产可用集群建议:
| 组件 | 节点数 | 最低配置 | 推荐配置 |
|---|---|---|---|
| TiDB Server | 2 | 8核16G | 16核32G |
| TiKV | 3 | 8核32G,SSD | 16核64G,NVMe SSD |
| PD | 3 | 4核8G | 8核16G |
| TiFlash(可选) | 2 | 16核64G,SSD | 32核128G,NVMe SSD |
TiKV 对磁盘 IO 非常敏感,强烈建议用 SSD,最好是 NVMe。机械硬盘跑 TiKV 基本等于自找麻烦,Raft 同步延迟会高得离谱。内存方面,TiKV 默认会占用较多内存做 Block Cache,建议至少 32G 起步。
网络方面,TiDB 集群内部通信量很大,建议万兆网卡起步,节点之间延迟控制在 1ms 以内。如果跨机房部署,要特别注意 PD 的 TSO 分配延迟,跨城机房可能导致事务性能急剧下降。
4.2 使用TiUP部署:一条命令搞定集群
TiUP 是 TiDB 官方推荐的部署工具,把复杂的集群部署简化成了几条命令。首先安装 TiUP:
curl --proto '=https' --tlsv1.2 -sSf https://tiup-mirrors.pingcap.com/install.sh | sh source ~/.bashrc然后写一个拓扑文件topology.yaml:
global: user: "tidb" ssh_port: 22 deploy_dir: "/tidb-deploy" data_dir: "/tidb-data" pd_servers: - host: 10.0.1.1 - host: 10.0.1.2 - host: 10.0.1.3 tidb_servers: - host: 10.0.1.4 - host: 10.0.1.5 tikv_servers: - host: 10.0.1.6 - host: 10.0.1.7 - host: 10.0.1.8 monitoring_servers: - host: 10.0.1.9 grafana_servers: - host: 10.0.1.9执行部署:
tiup cluster deploy my-cluster v6.5.0 topology.yaml --user root -p tiup cluster start my-cluster等几分钟,集群就起来了。用tiup cluster display my-cluster可以看到各节点状态。然后连上 TiDB 验证:
mysql -h 10.0.1.4 -P 4000 -u root提示:生产环境一定要部署监控组件(Prometheus + Grafana),TiDB 的监控面板非常详细,能提前发现很多问题。TiUP 默认会装好,别省这一步。
4.3 关键参数调优:让集群跑得更稳
部署完之后,有几个参数建议根据业务特点调整:
TiKV 侧:
[rocksdb] # 增加 Block Cache 大小,提升读性能 block-cache-size = "16GB" [raftstore] # 控制 Region 分裂速度 region-split-size = "96MB" # 控制 Raft 日志同步批量大小 raft-entry-max-size = "8MB"TiDB 侧:
# 控制单事务大小限制 txn-total-size-limit = 104857600 # 100MB # 控制并发执行算子数量 tidb_executor_concurrency = 8 # 开启慢查询日志 enable-slow-log = true slow-threshold = 300 # 单位毫秒PD 侧:
# 控制调度速度 leader-schedule-limit = 4 region-schedule-limit = 2048 max-snapshot-count = 64这些参数不是拍脑袋定的,要根据集群规模、业务读写比例、硬件配置来调。比如你的业务写多读少,可以适当增大 Raft 日志批量;如果读多写少,可以增大 Block Cache。调参之前最好在测试环境验证,别直接上生产。
4.4 数据迁移:从MySQL到TiDB的平滑过渡
从 MySQL 迁移到 TiDB,官方推荐用DM(Data Migration)工具。DM 支持全量迁移 + 增量同步,可以在业务不中断的情况下把数据搬过去。
部署 DM 集群:
tiup dm deploy dm-cluster v6.5.0 topology-dm.yaml --user root -p tiup dm start dm-cluster配置迁移任务task.yaml:
name: mysql-to-tidb source-id: mysql-replica-01 target-database: host: "10.0.1.4" port: 4000 user: "root" password: "" mysql-instances: - source-id: mysql-replica-01 block-allow-list: "global" mydumper-config-name: "global" loader-config-name: "global" syncer-config-name: "global" block-allow-list: global: do-dbs: ["your_database"]启动任务:
tiup dmctl --master-addr 10.0.1.9:8261 start-task task.yaml迁移过程中有几个坑要注意:第一,MySQL 和 TiDB 的字符集要一致,否则可能出现乱码;第二,自增主键在 TiDB 上如果没开AUTO_RANDOM,写入热点会很严重;第三,迁移完成后要做数据校验,官方有sync-diff-inspector工具可以对比两边数据。
5. 踩坑实录:TiDB生产环境常见问题与排查
5.1 写入热点:为什么你的TiKV节点负载不均
写入热点是 TiDB 新手最容易遇到的问题。现象是:三个 TiKV 节点,一个 CPU 跑到 90%,另外两个闲得发慌。原因通常是主键是连续自增的,所有写入都落在最后一个 Region 上。
排查方法:登录 Grafana 监控面板,看TiKV Dashboard -> Region -> Region Heatmap,如果发现某个 Region 的读写流量特别高,那就是热点了。
解决办法有三种:
- 使用
AUTO_RANDOM主键,让 TiDB 自动打散写入。 - 使用
SHARD_ROW_ID_BITS和PRE_SPLIT_REGIONS在建表时预切分。 - 业务层改造主键,比如用用户 ID 做哈希后缀。
-- 建表时预切分,适合批量导入场景 CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_RANDOM, user_id BIGINT, amount DECIMAL(10,2) ) SHARD_ROW_ID_BITS = 4 PRE_SPLIT_REGIONS = 4;5.2 慢查询频发:定位与优化实战
TiDB 提供了慢查询日志和INFORMATION_SCHEMA.SLOW_QUERY表,可以很方便地定位慢 SQL。
-- 查询最近10条慢查询 SELECT query_time, query, digest FROM information_schema.slow_query WHERE is_internal = false ORDER BY query_time DESC LIMIT 10;找到慢 SQL 后,用EXPLAIN ANALYZE看实际执行情况:
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 1001 AND status = 'paid';常见慢查询原因和对策:
| 原因 | 现象 | 对策 |
|---|---|---|
| 缺索引 | 全表扫描,cop_task数量巨大 | 加索引 |
| 统计信息过期 | 估算行数与实际差距大 | ANALYZE TABLE |
| 执行计划选错 | 走了错误的 Join 算法 | 用 HINT 强制 |
| 热点 Region | 单个 TiKV 负载高 | 打散写入 |
| 大事务 | 事务提交慢,锁冲突多 | 拆小事务 |
5.3 集群扩容与缩容:在线操作注意事项
TiDB 支持在线扩容,加 TiKV 节点后 PD 会自动调度数据过去。但扩容时要注意:
- 新节点加入后,PD 会开始搬迁 Region,这会占用网络和磁盘 IO。建议在业务低峰期操作。
- 可以通过
store-limit控制单个节点的调度速度,避免搬迁太猛影响在线业务。 - 缩容 TiKV 时,要先确认该节点上的 Region 已经全部迁走,再下线节点。用
tiup cluster scale-in会自动处理。
# 扩容 TiKV tiup cluster scale-out my-cluster scale-tikv.yaml # 缩容 TiKV tiup cluster scale-in my-cluster --node 10.0.1.8:201605.4 常见问题速查表
| 问题 | 可能原因 | 排查命令/方法 |
|---|---|---|
| 连接超时 | TiDB Server 负载高或网络问题 | tiup cluster display,看节点状态 |
| 事务提交慢 | PD TSO 分配慢或锁冲突 | 看 Grafana 的 Transaction 面板 |
| Region 不均衡 | PD 调度受限或节点故障 | pd-ctl store查看节点状态 |
| TiFlash 同步延迟 | 网络带宽不足或 TiFlash 负载高 | 看 TiFlash 的同步延迟监控 |
| OOM | 内存参数配置不当 | 调整block-cache-size和mem-quota-query |
注意:TiDB 的监控面板信息量很大,刚开始可能不知道看哪里。建议先关注几个核心指标:TiDB 的 Query Duration、TiKV 的 CPU 和 IO、PD 的 TSO Wait Duration。这几个指标正常,集群基本就没大问题。
6. 到底该不该上TiDB:场景判断与选型建议
6.1 适合TiDB的典型场景
不是所有业务都适合 TiDB。根据我的经验,以下几类场景上 TiDB 收益最明显:
第一,单表数据量超过 5000 万,且还在快速增长。MySQL 单表到这个量级,即使有索引,DDL 操作也会变得非常痛苦,加个字段可能锁表几十分钟。TiDB 的 DDL 是在线的,加字段秒级完成,不影响业务。
第二,需要水平扩展但不想分库分表。分库分表中间件(如 ShardingSphere)虽然能解决问题,但侵入性强,SQL 兼容性差,跨分片 Join 和事务很麻烦。TiDB 对应用透明,业务代码基本不用改。
第三,有 HTAP 需求,想用一套系统同时扛 OLTP 和 OLAP。以前的做法是 MySQL 扛在线业务,定时 ETL 到数仓跑分析。TiDB + TiFlash 可以省掉这套 ETL 链路,分析查询直接走列存副本,延迟低,架构简单。
第四,对高可用要求高,不能接受单点故障。TiDB 原生多副本,Raft 自动选主,机房级故障也能自动恢复。相比 MySQL 主从切换需要人工介入或者依赖外部组件,TiDB 的高可用是内建的。
6.2 可能不适合的情况
反过来,以下几种情况要慎重:
- 数据量很小,单机 MySQL 完全够用。比如几百万行数据,QPS 几百,上 TiDB 就是杀鸡用牛刀,运维复杂度反而增加。
- 对延迟极度敏感,要求 P99 在个位数毫秒。TiDB 的分布式事务和 Raft 同步会带来额外延迟,虽然优化后可以做到十几毫秒,但和单机 MySQL 的亚毫秒级还是有差距。
- 团队没有分布式数据库运维经验。TiDB 虽然自动化程度高,但出问题时排查需要一定的分布式系统知识。如果团队全是 MySQL DBA,转型需要学习成本。
- 预算非常紧张。TiDB 至少需要 3 个 TiKV 节点 + 3 个 PD 节点 + 2 个 TiDB 节点,硬件成本比单机 MySQL 高不少。虽然可以用低配机器,但性能会打折扣。
6.3 选型对比:TiDB vs MySQL vs 分库分表 vs NoSQL
| 维度 | MySQL | 分库分表 | TiDB | NoSQL(如HBase) |
|---|---|---|---|---|
| SQL 兼容性 | 完整 | 部分 | 高度兼容 MySQL | 有限 |
| 水平扩展 | 差 | 好 | 好 | 好 |
| 分布式事务 | 不支持 | 弱 | 完整 ACID | 通常不支持 |
| 运维复杂度 | 低 | 高 | 中 | 高 |
| 高可用 | 主从切换 | 依赖中间件 | 原生 Raft | 依赖外部 |
| 分析能力 | 弱 | 弱 | TiFlash 列存 | 弱 |
| 适用数据量 | 千万级 | 亿级 | 十亿级以上 | 百亿级 |
这张表不是绝对的,具体选型还要看业务特点。比如你的业务是纯 KV 查询,QPS 极高但事务简单,那 HBase 可能比 TiDB 更合适。但如果业务有复杂 SQL 和事务需求,TiDB 的优势就体现出来了。
6.4 迁移前的检查清单
如果你决定上 TiDB,迁移前建议对照这份清单逐项确认:
- SQL 兼容性审查:用官方工具
sync-diff-inspector和checker扫描现有 SQL,看有没有 TiDB 不支持的语法。 - 字符集确认:TiDB 默认
utf8mb4,和 MySQL 的utf8有差异,要确认业务侧没有依赖旧字符集。 - 自增主键改造:如果表用自增主键且写入量大,考虑改成
AUTO_RANDOM。 - 事务大小评估:检查有没有大事务,超过 100MB 的要拆分。
- 连接池配置:TiDB 对连接数的处理和 MySQL 不同,建议连接池最大连接数不要超过 1000。
- 监控告警配置:部署好 Prometheus + Grafana,配置关键指标告警。
- 回滚方案:迁移过程中保留 MySQL 原库,确认 TiDB 稳定运行一段时间后再下线。
我个人在实际操作中的体会是,TiDB 最大的价值不是性能比 MySQL 快多少,而是把水平扩展和高可用做成了数据库的内建能力,让业务开发不用再操心分库分表、主从切换这些脏活累活。但它也不是银弹,小数据量场景硬上 TiDB 只会增加复杂度。选型之前,先把自己的数据量、QPS、延迟要求、团队能力这几个维度想清楚,答案自然就出来了。最后再分享一个小技巧:TiDB 的官方文档和社区非常活跃,遇到问题先去 GitHub Issues 和 AskTUG 搜一下,大概率已经有人踩过同样的坑了。