You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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对应的行锁且未释放,当前事务会进入锁等待队列。结合你的代码来看:

    1. 调用obtainLock的事务是MANDATORY级别的,意味着锁的持有时间取决于调用方的事务生命周期——如果调用方开启了一个大事务,拿到锁后后续业务逻辑执行很久才提交,锁会被长时间持有,导致其他线程的getLock请求一直等待。
    2. 当多个线程同时抢同一个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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 15:19:57