乐观锁加重试,并发更新数据库一条记录导致:Lock wait timeout exceeded

背景:

  • mysql数据库,用户余额表有一个version(版本号)字段,作为乐观锁。
  • 更新方法有事务控制:
@Transactional(rollbackFor = Exception.class)
  • 更新时,比对版本号,如果版本号不一致,则更新失败。
  • 有重试机制,如果更新失败,则查询最新版本号,再次更新,重试超过5次,报错退出。
  • 更新的核心方法:
    public boolean updateUserAccount(Long userId, int amount) {
        boolean retryable;
        int attemptNumber = 0;
        do {
            // 查询最新版本号
            UserAccount userAccount = accountMapper.selectByPrimaryKey(userId);
            long oldVersion = userAccount.getVersion();

            // 更新
            boolean success = accountMapper.updateBalance(amount, new Date(), userId, oldVersion) > 0;

            if (success) {
                return true;
            } else {
                attemptNumber++;
                retryable = attemptNumber < 5;
                if (attemptNumber == 5) {
                    log.error("超过最大重试次数");
                    break;
                }

                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    log.error(e);
                }
            }
        } while (retryable);

        return false;
    }
  • 更新语句: 
    UPDATE user_account
    SET
    balance = balance - #{amount,jdbcType=INTEGER},
    update_time = #{updateTime,jdbcType=TIMESTAMP},
    version = #{version,jdbcType=BIGINT} + 1
    WHERE balance > #{amount,jdbcType=INTEGER}
    AND user_id = #{userId,jdbcType=BIGINT}
    AND version = #{version,jdbcType=BIGINT};

 在并发更新时,报异常:Lock wait timeout exceeded

分析:

根据日志分析出:
线程a、b几乎同时到达
线程a查询版本号:856
线程a更新数据库:成功
数据库当前版本号:857

线程b查询到的版本号:856(实际已不是最新)
线程b更新数据库:失败
线程b重试,查询版本号:856
线程b更新数据库:失败
。。。
线程b超过重试次数,退出

线程b重试的过程中,又有其他线程到来,比如c,d,e
线程c查询版本号:857
线程c更新数据库:阻塞,因为b拿到锁一直在重试

线程d查询版本号:857
线程d更新数据库:阻塞,因为b拿到锁一直在重试

线程b超次数退出后,c,d,e争抢锁
d拿到锁,更新数据库:成功
数据库当前版本号:858

线程c查询到的版本号:857(实际已不是最新)
线程c更新数据库:失败
线程c重试,查询版本号:857
线程c更新数据库:失败
。。。
线程c超过重试次数,退出

重试次数过多,事务执行时间超过mysql默认的锁等待时间(50s),就会报出:Lock wait timeout exceeded
为什么线程读不到最新的版本号呢?原来是用到了事务,且mysql默认事务隔离级别Repeatable Read,把隔离级别改为READ_COMMITTED,问题解决:
@Transactional(rollbackFor = Exception.class, isolation = Isolation.READ_COMMITTED)
分析了这么多,解决问题其实只需要一行代码。



 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章