【MySQL锁等待超时】
博客记录一次MySQL锁等待超时问题排查与解决
在项目日常迭代中,突然收到线上告警,核心业务接口出现异常,查看日志后发现是MySQL锁等待超时问题,特此记录整个排查、分析与解决过程,也希望能给遇到同类问题的小伙伴提供参考。
一、问题现象:明确报错信息
线上接口报错,核心错误信息如下:
【Lock wait timeout exceeded; try restarting transaction】
触发报错的SQL语句:【UPDATE table_name SET num = num + ? WHERE buyer_user_id = ?】
从报错信息来看,很直观的是MySQL锁等待超时——当一个事务持有某条数据的锁且未提交时,另一个事务尝试更新同一条数据,无法获取锁,等待时间超过MySQL默认的锁等待阈值(默认50秒),最终抛出该异常。
二、理论分析:锁等待超时的核心逻辑
先明确一个核心逻辑:MySQL InnoDB引擎下,更新操作(UPDATE)会对匹配的行加行级排他锁(X锁)。当多个事务同时操作同一条数据时,会出现锁竞争:
-
事务A执行UPDATE语句,对buyer_user_id = XXX的数据加行锁,开始执行后续逻辑,未提交事务;
-
事务B紧接着执行相同条件的UPDATE语句,尝试获取该数据的行锁,但此时锁被事务A持有;
-
事务B进入锁等待队列,等待事务A释放锁;
-
若事务A持有锁的时间过长,超过MySQL的innodb_lock_wait_timeout设置(默认50秒),事务B就会抛出「Lock wait timeout exceeded」报错。
简单来说,就是「前面的UPDATE还没提交,新的UPDATE又要操作同一条锁数据,新事务拿不到锁,执行超时」。
三、问题排查:从日志定位根因
好在项目日志有完整打印SQL语句和执行链路,结合日志排查,逐步锁定问题核心:
1. 日志检索,定位异常SQL
根据报错时间,往前检索日志,找到相同的UPDATE语句(UPDATE table_name SET num = num + ? WHERE buyer_user_id = ?),发现有一条该SQL执行后,未出现提交或回滚的日志记录——说明该事务未正常结束,一直持有锁。
2. 进一步定位:事务嵌套导致锁冲突
结合代码执行链路,发现了关键问题:
-
a. 锁冲突的核心:两条UPDATE语句更新的是同一条数据(buyer_user_id相同),锁定的数据有重叠;
-
b. 事务未提交的原因:外层方法开启了事务,包含该UPDATE操作;子方法又手动开启了新的事务,同时执行了相同条件的UPDATE操作;
-
c. 致命逻辑:执行链路陷入循环等待——子方法执行完后,外层事务才能提交;而外层事务的UPDATE执行完后,子方法的UPDATE才能获取锁执行,形成「事务与锁嵌套」的死循环,最终导致锁等待超时。
举个简单的执行链路示例,更易理解:
// 外层方法(开启事务)
@Transactional
public void outerMethod() {
// 执行UPDATE操作,加锁,未提交
updateTable(buyerUserId);
// 调用子方法(开启新事务)
innerMethod(buyerUserId);
// 外层事务提交(需等待子方法执行完成)
}
// 子方法(开启新事务)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void innerMethod(Long buyerUserId) {
// 执行相同条件的UPDATE,尝试获取锁(被外层事务持有)
updateTable(buyerUserId);
}
这种情况下,外层事务的UPDATE持有锁,等待子方法执行完成才能提交;子方法的UPDATE需要获取锁,却被外层事务持有,一直等待,最终子方法的事务触发锁等待超时。
四、核心解决方案:解决事务嵌套导致的锁冲突
针对排查出的「事务与锁嵌套」问题,核心解决方案只有一个:子方法沿用外层事务,使用同一个事务,避免多事务嵌套。
具体实现(基于Spring事务传播机制):
将子方法的事务传播机制改为默认的「PROPAGATION_REQUIRED」(无需手动设置,Spring默认就是该机制),即子方法不开启新事务,而是加入外层事务,共用同一个事务。
// 外层方法(开启事务)
@Transactional
public void outerMethod() {
updateTable(buyerUserId);
// 调用子方法(沿用外层事务)
innerMethod(buyerUserId);
// 外层事务提交,所有操作统一提交,锁一次性释放
}
// 子方法(沿用外层事务,无需开启新事务)
// 移除 @Transactional(propagation = Propagation.REQUIRES_NEW),或使用默认传播机制
public void innerMethod(Long buyerUserId) {
updateTable(buyerUserId);
}
修改后,外层事务和子方法的UPDATE操作共用一个事务,先执行外层的UPDATE,再执行子方法的UPDATE(同一事务内,锁不会冲突),最后统一提交事务,释放锁,彻底解决锁等待超时问题。
五、其他优化点:进一步避免锁等待超时(含适用场景+补充方案)
解决核心问题后,为了避免后续再出现类似问题,结合业务场景,补充了以下优化点,每个优化点均说明适用场景和可替代方案,方便根据实际业务选择。
优化点1:缩小事务代码范围
适用场景:事务内包含非核心操作(如日志写入、第三方接口调用、无关查询等),导致事务执行时间过长,锁持有时间久,易引发锁等待。
具体操作:事务内仅保留核心的数据库操作(如UPDATE、INSERT、DELETE),将日志写入、第三方接口调用、无关查询等耗时操作,移到事务外部执行。
补充方案:若某些耗时操作必须在事务内(如更新后需调用第三方接口同步数据),可采用「异步执行」方式,将耗时操作交给线程池异步处理,缩短事务执行时间。
优化点2:事务方法改为先后顺序,避免嵌套
适用场景:多个事务方法存在依赖关系,且均操作同一条数据,易出现嵌套导致的锁冲突;或多事务方法执行顺序混乱,引发锁竞争。
具体操作:梳理事务方法的执行逻辑,取消不必要的事务嵌套,改为线性执行顺序——先执行完一个事务的所有操作,提交后再执行另一个事务,避免同时持有锁。
补充方案:若业务必须嵌套,除了沿用外层事务,还可采用「分布式锁」(如Redis锁),对同一buyer_user_id的操作进行串行化,避免锁冲突。
优化点3:为UPDATE、事务设置超时时间
适用场景:非核心业务,允许偶尔更新失败,且能通过重试机制弥补;希望避免长时间锁等待导致的系统阻塞。
具体操作:
-
事务超时:在@Transactional注解中设置timeout属性,如@Transactional(timeout = 10),表示事务最长执行10秒,超时则回滚,释放锁;
-
UPDATE超时:通过MySQL参数设置,如set global innodb_lock_wait_timeout = 10,缩短锁等待时间(默认50秒)。
弊端:缩短超时时间后,可能出现事务回滚,导致数据不一致(如更新了部分数据后超时回滚);
补充方案:结合「重试机制」,超时后将任务放入Redis重试队列,间隔一定时间重试,同时做好数据一致性校验(如更新前查询数据状态),避免数据错乱。
优化点4:缩小UPDATE锁定的数据范围
适用场景:UPDATE语句的WHERE条件范围过大,导致锁定过多数据(甚至升级为表锁),引发大面积锁冲突;或WHERE条件未命中索引,导致全表扫描+表锁。
具体操作:
-
优化WHERE条件:确保UPDATE语句的WHERE条件精准,仅锁定需要更新的数据(如本次问题中的buyer_user_id,确保只锁定单个用户的数据);
-
添加索引:给UPDATE语句的WHERE条件字段(如buyer_user_id)添加唯一索引或普通索引,避免MySQL全表扫描,确保锁是行锁而非表锁。
补充方案:若需批量更新数据,避免一次性更新大量数据,改为分批更新(如每次更新100条),每批更新后提交事务,释放锁,减少锁持有时间和冲突概率。
六、总结
本次MySQL锁等待超时的核心原因,是「事务嵌套导致的锁冲突」——外层事务与子方法新事务同时操作同一条数据,形成循环等待,最终触发超时。
解决这类问题的关键的是:避免不必要的事务嵌套,确保操作同一条数据的事务串行执行,同时缩短锁持有时间、缩小锁范围。
另外,日志打印的重要性不可忽视,本次能快速定位问题,全靠日志完整记录了SQL语句和执行链路。后续开发中,也需注意事务的使用规范,避免因事务滥用导致锁相关问题,同时做好优化,提升系统稳定性。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)