解决 MySQL 报错 ERROR 1205 (HY000): Lock wait timeout exceeded

深入分析 MySQL 1205 锁等待超时报错,详细教你利用 information_schema.innodb_trx 定位阻塞事务并彻底解决死锁与长事务问题。

发布于

在生产环境中进行高并发写入、批量更新或复杂事务处理时,经常会在应用程序端捕获到以下异常:

ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

在 Java、Go 或 Python Web 服务中,这通常会导致接口超时、大量线程堵塞,甚至雪崩。

本文将带你从根本原理入手,教你使用标准 SQL 语句揪出“罪魁祸首”的阻塞事务,并提供针对生产环境的调优与防范策略。

报错原理深度拆解

MySQL InnoDB 存储引擎使用**行级锁(Row-level Locking)**来实现事务隔离。

  • 当事务 A 执行 UPDATE、DELETE 或 SELECT ... FOR UPDATE 时,会对匹配的数据行(以及索引间隙 Gap Lock)加锁。
  • 此时事务 B 也试图修改或锁定相同的行,事务 B 必须进入等待队列(Lock Wait)。
  • 如果事务 A 迟迟没有提交(COMMIT)或回滚(ROLLBACK),当事务 B 的等待时长超过配置的阈值时,InnoDB 就会抛出 ERROR 1205 (HY000) 并自动回滚事务 B 当前的等待语句。

该超时时间由全局变量 innodb_lock_wait_timeout 控制(默认通常为 50 秒)。


紧急排查:如何找出哪个事务阻塞了整个系统?

当系统大面积报 1205 错误时,单纯重启服务或调大超时时间只是掩耳盗铃,必须找出持有锁而不释放的慢事务或未提交事务。

步骤 1:查询当前未提交的长事务与锁等待

在 MySQL 控制台运行以下标准诊断 SQL:

SELECT 
    r.trx_id AS waiting_trx_id,
    r.trx_mysql_thread_id AS waiting_thread,
    r.trx_query AS waiting_query,
    b.trx_id AS blocking_trx_id,
    b.trx_mysql_thread_id AS blocking_thread,
    b.trx_query AS blocking_query,
    TIMESTAMPDIFF(SECOND, b.trx_started, NOW()) AS blocking_duration_secs
FROM performance_schema.data_lock_waits w
JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_engine_transaction_id
JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_engine_transaction_id;

[!TIP] 如果你的 MySQL 是 5.7 版本,请使用兼容视图: SELECT * FROM sys.innodb_lock_waits;

步骤 2:查看阻塞事务的详细执行状态

通过上面的查询拿到 blocking_thread(即源头连接的线程 ID,例如 1024),执行:

SELECT * FROM information_schema.innodb_trx WHERE trx_mysql_thread_id = 1024\G

查看源头事务是在哪一秒启动的(trx_started)、锁定了多少行(trx_rows_locked)。

步骤 3:紧急阻断(杀掉阻塞会话)

确认该阻塞连接来自某个卡死的脚本或未释放的事务后,可以在控制台直接 Kill 掉该线程以解救系统:

KILL 1024;

执行后,阻塞立即解除,排队的其他业务事务即可正常执行。


根治方案与系统调优

方案 1:检查并关闭 autocommit = 0 的隐患

很多 ORM 框架或数据库客户端会默认设置 SET autocommit = 0。 如果在代码中开启了事务而忘记写 commit() 或在发生异常的 except 分支中漏掉了 rollback(),这个连接就会一直保持事务状态并霸占行锁,直到连接超时断开。

最佳实践:

  • 严格遵循上下文管理器模式(如 Python with db.transaction(): 或 Spring @Transactional)。
  • 确保所有的 try...finally 块都有确定性的 rollback()。

方案 2:优化查询索引,杜绝行锁升级为表锁

在 InnoDB 中,如果 UPDATE 或 DELETE 语句的 WHERE 条件没有命中索引,InnoDB 就无法精确锁定特定记录,只能扫描全表并在扫描过程中锁住聚簇索引上的所有行与间隙! 这会导致哪怕更新一条完全不相关的数据,也会被阻塞并超时。

例如:

-- 危险!如果 phone 字段没有建立索引,该语句会锁死整个 users 表!
UPDATE users SET status = 'active' WHERE phone = '13800000000';

必须为高频更新条件的字段添加索引:

ALTER TABLE users ADD INDEX idx_phone (phone);

方案 3:避免在事务内部夹带耗时外部网络请求

在代码中编写事务时,严禁在事务边界内调用外部第三方 HTTP 接口、发送短信邮件或进行耗时复杂的计算。

❌ 错误模式:
[开启数据库事务]
  ├── 更新订单状态为待支付 (加锁)
  ├── 发起微信支付远程 HTTP 接口调用 (耗时 3~10 秒)
  └── [提交事务 (释放锁)]

✅ 正确模式:
[开启数据库事务]
  └── 更新订单状态并尽快 [提交事务 (释放锁)]
[事务结束后再执行外部网络调用]

方案 4:合理配置锁超时阈值

在某些对响应时间要求极高的高并发 OLTP 场景下,让用户等待 50 秒才超时体验极差。可以适当调小该阈值让应用快速失败重试:

在 my.cnf 中配置:

[mysqld]
# 将等待超时调整为 10 秒(默认为 50 秒)
innodb_lock_wait_timeout = 10

总结

出现 Lock wait timeout exceeded 时的标准排查三部曲:

  1. 查:用 sys.innodb_lock_waits 锁定源头阻塞线程 ID;
  2. 杀:用 KILL <thread_id> 紧急解除生产阻塞;
  3. 治:检查事务是否漏写 commit,以及 WHERE 条件是否走到了索引。