案例:接口超时,追查锁等待链¶
场景背景¶
- 时间:2026-06-18 周四 14:20
- 现象:用户反馈"提交订单"接口超时,超过 30 秒无响应
- 数据库:MySQL 8.0.32,业务库
jump - 环境:电商下单流程,涉及订单表、库存表、用户余额表更新
- 目标:定位是 SQL 慢,还是锁等待导致超时
生活化比喻:锁等待就像饭店里只有一个收银台。A 顾客(事务 A)在收银台结账,但一直在翻包找优惠券(长事务没提交)。B 顾客(事务 B)排队等着付款,越等越急,最后等超时了(接口报错)。dbskiter 就像监控摄像头,能看清谁占了收银台、谁在排队、排了多久。
排查步骤¶
1. 检查是否有锁等待(30 秒)¶
dbskiter --database=jump lock analyze
输出:
锁分析快照(2026-06-18 14:20:35)
====================================
当前锁等待: 3 个
锁等待详情:
[事务 8821] 持有锁 等待锁
用户: app_user@10.0.1.50
线程: 42
状态: RUNNING
SQL: UPDATE inventory SET stock = stock - 1 WHERE product_id = 10086
已执行时间: 45s
持有锁:
- 表: inventory (行锁: product_id=10086)
[事务 8822] 等待锁
用户: app_user@10.0.1.51
线程: 58
状态: WAITING
SQL: UPDATE inventory SET stock = stock - 1 WHERE product_id = 10086
等待锁: inventory.product_id=10086 (行锁)
等待时间: 38s
阻塞者: 事务 8821
[事务 8823] 等待锁
用户: app_user@10.0.1.52
线程: 71
状态: WAITING
SQL: UPDATE inventory SET stock = stock - 1 WHERE product_id = 10086
等待锁: inventory.product_id=10086 (行锁)
等待时间: 32s
阻塞者: 事务 8821
关键发现: 1. 事务 8821 正在执行
UPDATE inventory,已执行 45 秒,持有product_id=10086的行锁 2. 事务 8822、8823 都在等同一个行锁,已等待 38 秒和 32 秒 3. 3 个事务都在更新同一商品的库存,形成锁等待链
2. 查看锁等待链(1 分钟)¶
dbskiter --database=jump lock chains
输出:
锁等待链:
====================================
链 1(长度: 3):
根阻塞者: 事务 8821 (app_user@10.0.1.50)
└─ 等待者: 事务 8822 (app_user@10.0.1.51) 等待 38s
└─ 等待者: 事务 8823 (app_user@10.0.1.52) 等待 32s
影响评估:
阻塞时间: 45s
受影响事务: 2
风险等级: HIGH
建议: 终止事务 8821 或优化其 SQL
分析:这是一个典型的热点商品锁竞争。3 个用户同时抢购
product_id=10086,第一个事务持有锁 45 秒不释放,后面两个事务排队等待,导致接口超时。
3. 查看阻塞者事务的完整 SQL(30 秒)¶
dbskiter --database=jump diagnose sql "
SELECT id, user, host, db, command, time, state, info
FROM information_schema.processlist
WHERE id = 8821;
"
输出:
id: 8821
user: app_user
host: 10.0.1.50
db: jump
command: Query
time: 45
state: Sending data
info: UPDATE inventory SET stock = stock - 1 WHERE product_id = 10086;
SELECT * FROM orders WHERE user_id = 10001;
INSERT INTO order_logs ...;
关键发现:事务 8821 不是一个简单的
UPDATE,而是包含多条 SQL 的长事务(UPDATE + SELECT + INSERT),整个事务执行了 45 秒还没提交。
4. 检查事务完整信息(30 秒)¶
dbskiter --database=jump diagnose sql "
SELECT
trx_id,
trx_mysql_thread_id,
trx_state,
trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS trx_seconds,
trx_tables_locked,
trx_rows_locked
FROM information_schema.innodb_trx
WHERE trx_mysql_thread_id = 8821;
"
输出:
trx_id: 45821
trx_mysql_thread_id: 8821
trx_state: RUNNING
trx_started: 2026-06-18 14:19:50
trx_seconds: 45
trx_tables_locked: 1
trx_rows_locked: 1
确认:事务已运行 45 秒,锁定了 1 张表的 1 行数据。
解决方案¶
紧急恢复(立即)¶
方案:终止阻塞事务(让后续事务继续执行)
# 先确认事务信息(再次确认)
dbskiter --database=jump lock analyze
# 终止事务 8821(注意:确认该事务可以安全回滚后执行)
dbskiter --database=jump lock kill 8821
# 验证锁等待是否解除
dbskiter --database=jump lock analyze
结果:事务 8821 被回滚,事务 8822 和 8823 立即获得锁,继续执行。接口超时恢复。
根治方案(防止再次发生)¶
方案 1:优化事务粒度(推荐)
// 错误示例:长事务,包含多个不相关的操作
transaction {
updateInventory(productId); // 扣库存
selectOrders(userId); // 查订单(不需要在事务里)
insertOrderLog(orderId); // 插日志(可以异步)
// 事务持有锁 45 秒
}
// 正确示例:最小事务,只保留必要的操作
transaction {
updateInventory(productId); // 扣库存(核心操作,必须原子性)
insertOrder(orderId); // 插订单(核心操作)
// 事务应在 100ms 内完成
}
// 以下操作移出事务,异步执行
selectOrders(userId); // 查询不需要事务
insertOrderLogAsync(orderId); // 日志异步,不影响主流程
方案 2:优化库存扣减逻辑(热点商品)
-- 错误:直接 UPDATE 行,锁竞争严重
UPDATE inventory SET stock = stock - 1 WHERE product_id = 10086;
-- 正确:使用乐观锁(版本号)或 Redis 预扣库存
-- 方案 A:乐观锁(适合库存充足,冲突少)
UPDATE inventory
SET stock = stock - 1, version = version + 1
WHERE product_id = 10086 AND stock >= 1 AND version = ?;
-- 方案 B:Redis 预扣 + 异步落库(适合秒杀场景)
-- 1. Redis DECR stock:10086
-- 2. 返回成功,异步发送 MQ 更新 MySQL
-- 3. MySQL 压力大幅降低,无锁竞争
方案 3:调整数据库隔离级别(权衡一致性和并发)
-- 默认 REPEATABLE READ(MySQL 默认)
-- 如果业务允许,可以降为 READ COMMITTED,减少锁持有时间
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 或者在 my.cnf 中设置全局
[mysqld]
transaction_isolation = READ-COMMITTED
注意:降级隔离级别会影响幻读和不可重复读,需确认业务逻辑是否允许。
方案 4:增加索引减少锁范围
-- 如果 WHERE 条件不走索引,可能锁全表或锁大量行
-- 确保 inventory.product_id 有主键或唯一索引
ALTER TABLE inventory ADD PRIMARY KEY (product_id);
-- 或
ALTER TABLE inventory ADD UNIQUE KEY uk_product_id (product_id);
验证效果¶
# 优化后持续监控锁等待
dbskiter --database=jump lock analyze
# 预期结果:锁等待数 = 0,或偶尔 < 3 且等待时间 < 1s
| 指标 | 优化前 | 优化后 | 目标 |
|---|---|---|---|
| 锁等待事务数 | 3 | 0 | 0-1 |
| 最长等待时间 | 38s | < 1s | < 5s |
| 事务执行时间 | 45s | < 100ms | < 500ms |
| 接口超时率 | 15% | < 0.1% | < 1% |
复盘要点¶
1. 锁等待排查口诀¶
接口超时 → 看锁等待 → 看锁链 → 找阻塞者 → 看事务时长 → 优化事务粒度 or 终止阻塞
2. 锁等待类型速查¶
| 锁类型 | 典型场景 | 排查命令 | 解决思路 |
|---|---|---|---|
| 行锁竞争 | 热点数据更新(库存、余额) | lock analyze |
最小事务、乐观锁、Redis |
| 表锁等待 | MyISAM 表或 DDL 操作 | lock analyze |
升级 InnoDB、避免高峰 DDL |
| 间隙锁 | 范围查询 + 插入(RR 隔离级别) | lock analyze |
降级 RC、避免范围锁 |
| 元数据锁(MDL) | DDL 阻塞 DML(加字段、索引) | lock analyze |
Online DDL、避免高峰 |
| 死锁 | 两个事务互相等待 | lock deadlocks |
调整执行顺序、重试机制 |
3. 事务优化原则¶
| 原则 | 说明 | 示例 |
|---|---|---|
| 最小事务 | 只把必要的操作放事务里 | 查询、日志移出事务 |
| 最短事务 | 事务应在 100ms 内完成 | 避免循环、批量处理 |
| 先读后写 | 先查询再更新,减少锁持有时间 | 确认库存足够再扣减 |
| 同序访问 | 多个事务按相同顺序访问资源 | 避免死锁 |
| 避免高峰 DDL | 加索引、改字段避开业务高峰 | 凌晨 2-4 点执行 |
4. 常见误区¶
| 误区 | 正确做法 |
|---|---|
| "锁等待就杀进程" | 先分析阻塞者是谁、为什么慢,能优化就不杀 |
| "事务越大越安全" | 事务越大锁持有时间越长,竞争越严重 |
| "SELECT 不放事务里没事" | SELECT ... FOR UPDATE 也会加锁 |
| "死锁和锁等待是一回事" | 锁等待是排队等,死锁是循环等待,MySQL 会自动回滚死锁 |
| "加索引就能解决锁问题" | 索引能减少锁范围,但热点数据竞争需要业务层优化 |
5. 锁监控建议¶
# 每日检查锁等待情况
dbskiter --database=jump lock analyze
# 每周生成锁报告
dbskiter --database=jump lock report
# 告警阈值建议
# 锁等待事务数 > 5: 警告
# 锁等待时间 > 10s: 严重
# 死锁次数 > 10/天: 需要优化
# 检查死锁历史
dbskiter --database=jump lock deadlocks
延伸阅读¶
- 案例:业务卡顿查慢查询
- 案例:CPU 飙升根因分析
- 案例:连接数打满
- dbskiter 命令:
lock analyze、lock chains、lock deadlocks、lock report