樂觀鎖加重試,併發更新數據庫一條記錄導致: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)
分析了這麼多,解決問題其實只需要一行代碼。



 

 

 

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