【TiDB 插入性能优化实战:从 5 秒到毫秒级的跨越】
TiDB 插入性能优化实战:从 5 秒到毫秒级的跨越
背景与问题现象
前提条件:
- 数据库:TiDB(分布式关系型数据库)
- 技术栈:JDK8 + MyBatis-Plus
- 表结构:11 个字段,数据量约 200 万条
异常现象:
线上环境发现某张表的 insert 操作异常缓慢,单次插入耗时高达 5 秒,远超业务预期(正常应在 100ms 以内)。从 TiDB 慢日志来看,所有慢 SQL 均为简单插入语句(如 insert into table (col1, col2...) values (...)),且无复杂逻辑或大字段。
排查过程:一步步定位根因
排查 1:排除连接池资源瓶颈
首先检查数据库连接池配置:最大连接数 max-active=100,通过监控发现同一秒内执行的 SQL 记录不足 80 条,远未达到连接池上限。因此,连接数不足并非问题原因。
排查 2:聚焦 TiDB 事务执行链路
通过 TiDB 慢日志的“执行链路详情”发现,耗时几乎全部集中在 Prewrite 阶段(分布式事务两阶段提交的第一阶段)。
补充:Prewrite 阶段是 TiDB 分布式事务的核心环节,需向事务涉及的所有 TiKV Region 的 Leader 发送请求,写入数据、记录锁信息并验证冲突。该阶段耗时直接取决于涉及的 Region 数量和 TiKV 处理能力。
排查 3:发现 ID 生成策略的矛盾
检查表结构定义,发现主键字段明确配置了 AUTO_INCREMENT(自增主键),但查询数据库中已插入的 ID 时,却发现其毫无自增规律(数值为 16 位以上的大整数,且跳跃性极大)。
这是一个明显的异常点:表定义的自增策略与实际 ID 分布不匹配。
排查 4:定位代码层的 ID 生成配置
查看项目代码的 MyBatis-Plus 全局配置,发现:
mybatis-plus:global-config:db-config:id-type: ASSIGN_ID # 全局使用雪花算法生成 ID
同时,实体类 Entity 的主键字段 id 未单独指定 @TableId 注解(即未覆盖全局配置)。
结论:代码中全局启用了雪花算法生成 ID,覆盖了 TiDB 表的 AUTO_INCREMENT 配置,导致实际插入的 ID 为雪花 ID(跳跃性大),而非预期的连续自增 ID。
解决方案:统一 ID 生成策略
针对排查结果,通过代码调整统一 ID 生成策略:
-
实体类指定自增策略:在主键字段添加注解,覆盖全局雪花算法配置:
public class TargetEntity {// 指定使用数据库自增策略,覆盖全局的 ASSIGN_ID@TableId(type = IdType.AUTO) private Long id;// 其他字段... } -
验证效果:重新部署后,新插入的数据 ID 按
1,2,3...连续递增,符合 TiDBAUTO_INCREMENT预期。慢日志显示insert耗时从 5 秒降至 50ms 以内,Prewrite 阶段耗时显著减少。
深度分析:五个关键问题解答
问 1:为什么雪花算法会导致插入变慢?
核心原因是 ID 分布分散导致 Region 数量激增:
- 雪花算法生成的 ID 由“时间戳+机器 ID+序列号”组成,看似递增但存在大范围跳跃(如多实例部署时,不同机器 ID 生成的 ID 差距极大)。
- TiDB 按主键范围分裂 Region(默认 64MB/Region),跳跃的 ID 会让 200 万条数据散落在数百个小 Region 中。
- Prewrite 阶段需要向每个涉及的 Region Leader 发送请求,Region 数量越多,总耗时越长(5 秒本质是数百个 Region 的交互延迟叠加)。
问 2:历史 200 万数据是否需要优化?如何优化?
是否需要优化:取决于历史数据的 Region 分布和操作性能:
- 若历史数据的 Region 数量超过正常范围(200 万数据应集中在 2-3 个 Region),或涉及历史数据的查询/更新耗时高,则需要优化。
优化方法:
-
无锁重组织 Region(推荐):
通过 TiDB 的REORGANIZE PARTITION按历史 ID 范围合并分散的 Region:-- 按历史雪花 ID 的实际范围合并(替换为实际 ID 区间) ALTER TABLE your_table REORGANIZE PARTITION BETWEEN (1658555588888888888) AND (1658555588888988888) INTO (PARTITION p_history VALUES LESS THAN (1658555588888988889)); -
重建表(适合非核心业务):
低峰期通过临时表按 ID 排序重写数据,强制数据集中分布:CREATE TABLE temp_table LIKE your_table; INSERT INTO temp_table SELECT * FROM your_table ORDER BY id; -- 按 ID 排序插入 RENAME TABLE your_table TO old_table, temp_table TO your_table;
问 3:如何判断 Region 数量和分散程度?
1. 查看 Region 数量
- TiDB Dashboard:登录
http://{tidb-ip}:2379/dashboard→ “数据分布” → “表分布”,搜索目标表查看“Region 数量”。 - SQL 查询:
正常范围:数据量(MB)/64(默认 Region 大小),200 万数据(约 100MB)应在 2-3 个。SELECT COUNT(DISTINCT region_id) AS region_count FROM information_schema.tikv_region_status WHERE table_name = 'your_table';
2. 判断 Region 是否分散
-
TiDB Dashboard:查看表的“Region 列表”,观察“范围起始(Start Key)”和“范围结束(End Key)”:
- 连续分布:Region 范围依次衔接(如
[1,10000)、[10000,20000))。 - 分散分布:范围跳跃极大(如
[1658...888, 1658...900)、[1658...950, 1658...999))。
- 连续分布:Region 范围依次衔接(如
-
SQL 验证:查询 Region 范围并检查连续性:
SELECT region_id, hex(range_start) AS start_key, hex(range_end) AS end_key FROM information_schema.tikv_region_status WHERE table_name = 'your_table' ORDER BY range_start;
问 4:若沿用雪花算法,200 万数据会慢吗?
大概率会慢。
雪花算法的 ID 跳跃性会导致 200 万数据分散在数百个 Region 中,引发:
- 插入时 Prewrite 阶段需与大量 Region 交互,耗时随 Region 数量线性增加;
- 查询/更新时需扫描多个分散 Region,Coprocessor 处理耗时飙升;
- 大量小 Region 消耗 TiKV 内存和 PD 调度资源,间接降低集群性能。
仅在“单实例部署+ID 严格连续+无范围查询”的极端场景下,性能问题可能不明显,但分布式系统中几乎不满足。
问 5:雪花算法与主键自增的适用场景对比
| 维度 | 雪花算法 | 主键自增(如 TiDB AUTO_INCREMENT) |
|---|---|---|
| 全局唯一性 | 支持(跨库/跨服务) | 单库内唯一,TiDB 分布式自增支持全局唯一 |
| ID 特征 | 整体递增,可能跳跃 | 严格连续(趋势) |
| 数据分布 | 可能分散(依赖 ID 生成规则) | 集中(按 ID 范围存储) |
| 适用场景 | 1. 跨库/跨服务全局唯一 ID 需求; 2. 不依赖数据库生成 ID; 3. 需要通过 ID 反推时间戳 | 1. 单集群内业务,无跨库唯一需求; 2. 频繁范围查询/排序; 3. 依赖 ID 连续性(如订单号); 4. 基于 TiDB 等分布式数据库优化性能 |
总结
本次案例的核心教训是:分布式数据库的 ID 生成策略需与存储特性匹配。TiDB 等分布式数据库依赖主键范围实现数据集中存储,雪花算法的 ID 跳跃性会破坏这一特性,导致性能问题。
实际开发中,应根据业务场景选择 ID 策略:需全局唯一时用雪花算法(但需优化 ID 分布),单集群内优先用数据库自增(尤其是 TiDB 的分布式自增),才能充分发挥分布式数据库的性能优势。
