【杂谈】死锁?NO,时间跳跃!

在日常开发或线上运维中,我们经常会遇到各种数据库异常,例如超时、死锁等。但有些问题,表面看似平常,背后却藏着意想不到的原因。

今天就分享一次由服务器时间跳跃引发的 MySQL 获取锁超时问题的排查过程。

问题现象:大量锁超时日志出现

某天系统日志中突然频繁出现如下报错信息:

复制代码
Caused by: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction

获取锁超时导致事务失败。

初步分析:死锁?

可能是出现死锁了,于是根据异常栈定位到问题代码,但发现该方法逻辑简单,仅修改一个entity,类似下面这样。(非真实业务代码)

复制代码
// 仅更新用户的最后访问时间
user.setLastVisitTime(LocalDateTime.now());
userRepository.save(user);

强行分析(猜想),这个修改是每个请求都会改到的,由于在请求事务内,事务没提交就会一直锁着,直到请求完成。

但一个长期稳定运行的项目,请求不太可能突然变慢

深入排查:慢日志未出现异常

如果出现死锁,那么慢日志里面一定有记录。但实际排查袭来,慢日志并无User相关的慢查询。

蛛丝马迹:不太常见的日志

复制代码
om.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Thread starvation or clock leap detected (housekeeper delta=1m26s857ms76µs413ns).

翻阅日志,发现一条clock leap detected的异常记录。于是验证服务器时间,发现比本地环境快了40多秒。

真相大白:服务器时间跳跃引发误判

MySQL进行锁等待和事务超时时,依赖系统时间戳进行判断。当系统时间突然跳跃到未来时间,导致MYSQL误判。

至于跳跃原因,推测是 NTP 客户端在检测到时间漂移后进行了强制同步(stepping)操作,瞬间将时间快进了几十秒。

更近一步:有哪些操作会导致获取锁超时?

在 MySQL 使用 InnoDB 引擎的前提下,锁超时Lock wait timeout exceeded)的出现通常有两个主要诱因:

1. 死锁(Deadlock)

最常见的原因就是死锁。死锁往往由于多个事务以不同顺序下修改相同资源,彼此持有对方需要的锁,造成互相等待、永远无法释放。

比如:

  • 事务 A 修改顺序是:先改用户,再改订单;

  • 事务 B 修改顺序是:先改订单,再改用户;

  • 双方各自持有一个锁,又想获取对方的,结果就死锁了。

MySQL 会检测到死锁并主动中断其中一个事务。(这时候日志里就会出现Dealock报错了)

其实,只要在项目中统一规定 Entity 的修改顺序,大部分死锁是可以避免的。

2. 长事务导致的锁未及时释放

InnoDB 中,事务未提交期间会一直持有锁。如果事务执行时间过长,会导致其他并发请求长时间阻塞,最终抛出锁等待超时异常。

事务执行过长,常见原因包括:

  • Entity 修改过多

    比如循环中逐个修改并保存,每次都 save(),反复刷 SQL。

  • 事务中包含耗时操作

    例如调用外部服务、HTTP 接口、微服务 RPC 等,尤其是对慢接口没有超时控制时。

  • 事务中存在显式等待

    Thread.sleep() 用于调试、限速等场景,期间锁不会释放。