Spring+MySQL分布式锁中select ... for update查询性能缓慢排查
我在Spring应用中基于MySQL实现了分布式锁,核心代码如下:
锁获取方法
@Transactional(propagation = Propagation.MANDATORY, isolation = Isolation.READ_COMMITTED) public void obtainLock(String lockKey) { entityManager.flush(); // Flush any pending INSERTs/UPDATEs to the DB before taking the lock Optional<Lock> lock = lockRepository.getLock(lockKey); if (lock.isEmpty()) { try { lockRepository.createLock(lockKey); entityManager.flush(); // Make sure the INSERT is issued to the DB immediately lockRepository.getLock(lockKey); } catch (Exception ex) { try { Thread.sleep(3000L); lockRepository.getLock(lockKey); } catch (Exception e) { if (e instanceof InterruptedException) { // Restore interrupted state... Thread.currentThread().interrupt(); } } } } }
LockRepository定义
public interface LockRepository extends Repository<Lock, String> { @Modifying @Query(value = "insert ignore into Locks (lockKey) values (:key)", nativeQuery = true) void createLock(@Param("key") String key); @org.springframework.data.jpa.repository.Lock(LockModeType.PESSIMISTIC_WRITE) @Query("select l from Lock l where l.lockKey = :key") Optional<Lock> getLock(@Param("key") String key); }
通过MySQL慢查询日志发现,lockRepository.getLock(lockKey)对应的SQL(select ... for update)平均耗时5秒,部分请求耗时长达数十秒。已知lockKey是表的主键/唯一索引,该SQL只会锁定单条记录,请问性能缓慢的原因可能是什么?
锁等待(最核心原因):
虽然select ... for update是行级锁,但如果有其他事务已经持有该lockKey对应的行锁且未释放,当前事务会进入锁等待队列。结合你的代码来看:- 调用
obtainLock的事务是MANDATORY级别的,意味着锁的持有时间取决于调用方的事务生命周期——如果调用方开启了一个大事务,拿到锁后后续业务逻辑执行很久才提交,锁会被长时间持有,导致其他线程的getLock请求一直等待。 - 当多个线程同时抢同一个
lockKey时,第一个线程通过createLock插入记录后,在同一个事务内调用getLock会持有该行锁;其他线程的createLock因insert ignore不会报错,但后续调用getLock会等待第一个线程的事务提交/回滚,并发量越大,等待队列越长,耗时就会被拉得越长。
- 调用
数据库连接池资源耗尽:
如果应用的数据库连接池配置不合理(比如max-size过小),当并发请求量超过连接池最大可用连接数时,线程会等待空闲连接,这部分等待时间会被计入getLock方法的耗时中,导致慢查询日志显示该SQL耗时过长。锁等待超时阈值的影响:
MySQL默认的innodb_lock_wait_timeout是50秒,如果某个事务持有锁的时间接近这个阈值,后续等待的请求就会出现数十秒的耗时;如果等待超时,还会抛出Lock wait timeout exceeded异常。索引有效性问题(排查项):
虽然你提到lockKey是主键,但仍需确认是否存在索引失效的情况:比如代码中传入的lockKey数据类型与数据库字段类型不匹配(比如数据库是char,代码传varchar带空格),导致MySQL无法使用主键索引,转而进行全表扫描,同时加锁范围扩大,引发更严重的等待。可以通过EXPLAIN执行计划验证是否走了主键索引。事务隔离级别与锁机制的隐性影响:
即使你在方法上设置了READ_COMMITTED隔离级别,如果MySQL全局隔离级别是默认的REPEATABLE READ,且未正确覆盖方法级别的设置,InnoDB可能会在某些场景下(比如幻读防护)额外加间隙锁,但这种情况在主键精准匹配的场景下概率极低,可作为次要排查点。
内容的提问来源于stack exchange,提问作者zonyang

