0
0
0
0
博客/.../

一个 4000 行的大事务,把政务云的 TiDB 锁卡了 9 分钟

 cationlin  发表于  2026-09-28
原创互联网

背景:政务云上的批量对账

上个月我们在一个区县级政务云上做了一套民生资金的对账系统。底层数据库用的是 TiDB 6.5,集群是 3 台 TiDB、3 个 TiKV、2 个 PD,全都在政务云里,资源规格不算高,TiKV 给的是 16C 64G,磁盘是本地 SSD。

这套系统白天是零散的查询和少量写入,不重。但每天凌晨 2 点会跑一个批量对账任务:把前一天各业务系统落库的流水拉过来,跟财务总账比对,一次事务里要更新 4000 行左右的记录。写这段逻辑的是业务组的同事,他觉得 4000 行"不算多",就包在一个大事务里一口气提了。

BEGIN;
UPDATE account_balance SET total = total - amount, updated_at = NOW()
  WHERE biz_no = ?;          -- 循环 4000 次
UPDATE settle_detail SET status = 'CHECKED', check_time = NOW()
  WHERE id IN (...);        -- 一次 200 条
COMMIT;

前两周都正常,凌晨跑完也就两三分钟。问题出在第三周,对账任务跑到一半,整个 TiDB 集群突然慢得离谱,应用端连接池全满,白天有人来查数据的时候,一个简单 SELECT 都要等十几秒。

现象:一个慢查询拖死整张表

我先登 Grafana 看监控,几个数字不对劲:

  • 一个 TiKV 节点的 CPU 长期挂在 90%+,另外两台才 40%
  • TiDB 的 txn_duration_seconds 里有一大批 5~8 秒的事务
  • 慢日志里全是同一张 account_balance 表的读写在排队
  • region_conflict 指标在批量任务启动那几分钟飙到几千

最扎眼的是:那个批量任务的事务,提交时间从平时的 30 秒,直接拉到了 9 分钟。9 分钟一个大事务不提交,意味着它持有的锁一直没释放,后面所有碰这几百个 Region 的写请求,全在等。

根因:大事务 + 热点 Region 的双重放大

先把当时还在挂着的事务捞出来看:

SELECT txn_id, state, txn_start, TIMESTAMPDIFF(SECOND, txn_start, NOW()) AS dur_s
FROM information_schema.cluster_transactions
WHERE state = 'RUNNING' AND TIMESTAMPDIFF(MINUTE, txn_start, NOW()) > 1
ORDER BY dur_s DESC;

跑出来一眼就看到那个 9 分钟的事务。再用 tidb-trace 抓了链路,问题很清楚,是两个因素叠在一起:

第一,一个事务里改了 4000 行、跨了 200 多个 Region。TiDB 是分布式两阶段提交,每碰一个新 Region,都要在预提交阶段给那个 Region 发一次 Prewrite。Region 越多,Prewrite 的网络往返越多,事务本身就更长。

第二,这 4000 行的分布不均匀。有个别 biz_no 是热点账户,对应的行集中在少数几个 Region 上,这几个 Region 就成了写热点。

结果就是:大事务占住 Region 锁 → 热点 Region 排队 → 别的业务想写这几行都得等 → 越等越多 → 提交阶段(Commit)要等所有 Region 的 prepare 都落盘确认,整个事务被拖到 9 分钟才松手。

我拉了那 9 分钟里各阶段的时间分布,一张表能说清楚:

应用攒 4000 行 UPDATE ~60s 业务循环,正常
Prewrite(跨 200+ Region) ~4 min Region 多,往返放大
等锁(热点 Region 冲突) ~4 min 被前面的写堵住
Commit ~1 min 等所有 Region 确认

处理:三刀切下去

第一刀:把大事务拆成小事务

最直接的是别用一个事务包 4000 行。业务侧把批量改写成每 200 行一个小事务,中间不跨,单个事务只碰 10 个以内的 Region。这样:

  • 单个事务的锁持有时间从"9 分钟"级降到"秒"级
  • 即使某个小事务慢了,后面不排队,锁很快释放
  • 出错了重跑粒度也小,不用全量重来

第二刀:热点打散 + 参数兜底

那几个热点 biz_no 的锁竞争太厉害。我们把 account_balance 的主键从 biz_no 单列,改成了 biz_no + 随机分片位 做前缀打散,让同一账户的余额更新落到多个 Region,避免全压在一个 Region 上。这是典型的热点行打散,TiDB 里对这种"少数 key 扛大量写"的场景很有效。

打散的同时在配置上加了两个兜底保护:

-- 会话级:调大 Commit 阶段的批量提交,减少 Commit 网络往返
SET @@txn_commit_batch_size = '128';

集群侧把 txn.max-commit-txn 调大到 5000,允许大事务一次提交更多 Region 而不被 too many transactions 直接打回,配合拆小事务后作为兜底(这属于 server 配置,改完 TiDB 需重启或 reload 生效)。

同时把批量任务的错峰时间从 2 点挪到 3 点半,避开白天偶发的写入高峰,让锁竞争窗口更小。

-- 拆分后的小事务(每 200 行一次提交,锁范围可控)
BEGIN;
UPDATE settle_detail SET status='CHECKED' WHERE biz_no=? AND batch=200;
COMMIT;

效果:从 9 分钟回到 15 秒

三刀下去,再跑同一天的对账:

  • 单个事务锁持有时间回到 15 秒以内
  • 批量整体耗时从 9 分钟降到 7 分钟,但不再卡库——因为锁是分段释放的,白天查询完全不受影响
  • 那个最热 Region 的 region_conflict 从几千降到个位数
  • CPU 那台 90% 的 TiKV 回落到 55%,三台基本拉平

最关键的是,慢日志里那条"SELECT 卡 10 秒"彻底消失了,白天的业务恢复丝滑。

复盘:大事务是分布式数据库的头号杀手

回头看,坑不在 4000 行本身,而在"用集中式数据库的直觉写分布式事务"。集中式里一个事务锁几行,锁释放快,问题不大;但在 TiDB 这种分布式架构里,一个事务牵动的 Region 越多,两阶段提交的放大效应越恐怖——锁持有时间 = 事务时长 × Region 扇出。

三句话给后面写批量任务的人:

  • 单事务别跨太多 Region,控制在几十行以内,大活儿拆小
  • 少数热点 key 扛大量写的,做主键打散,别让一个 Region 独吞
  • 批量任务错峰跑,别跟白天写入抢同一批 Region

分布式数据库不是"更大更快的 MySQL",它的锁、事务、扩缩容都是跨节点协调的。理解了这个,很多" inexplicable 的卡顿"就有解释出处了。

0
0
0
0

版权声明:本文为 TiDB 社区用户原创文章,遵循 CC BY-NC-SA 4.0 版权协议,转载请附上原文出处链接和本声明。

评论
暂无评论