跳转至

案例:接口超时,追查锁等待链

场景背景

  • 时间: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

延伸阅读