mysql事物默認隔離級別下樂觀鎖(CAS)重試數據版本不更新的問題

異常信息

Ccom.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction

問題發生緣由

樂觀鎖修改數據的時候,  數據版本號(version)已經被修改了,致使修改失敗. 進行重試修改時, 每次從數據庫讀取出來的數據不是數據庫最新版本, 致使無限次重試, 直到該事物超時自動退出java

使用樂觀鎖(CAS)機制去更新一條記錄, 從數據庫查詢一條數據, 當查出來數據版本號是1, 在修改數據的時候, 把數據版本號做爲修改條件之一,​update `table_name` set version=`2` where version=`1`​ , 若是修改失敗, 那麼該數據已經被更新過了, 從新讀取數據, 進行重試,  而Mysql數據庫中, 開啓事物以後, 在當前事物中, 屢次查詢的值, 會是同一個, 即修改失敗, 進行從新讀數據, 獲得的數據版本號是該次事物中第一次讀的時候的版本號 (即數據庫事物隔離級別中的可重複讀, 避免髒讀現象(屢次讀取值不一致),  相似於開啓一個事物的時候, 每次讀取, 會把查詢的數據複製到事物空間, 當前事物讀數據庫的時候, 不會讀表中的實際數據, 而是讀事物空間的數據.  由於這個緣由, 版本號始終不會跟新, 因此會一直修改失敗.mysql

問題代碼表示

`[@Override](https://my.oschina.net/u/1162528)`

`@Transactional(rollbackFor = Exception.**class**) // 開始事物`

`**public** Tuple2<TradeVO, WalletVO> earn(EarnDTO earnDTO) {`

`**return** retryEarn(earnDTO);// remaind 重試在事物內部`

`}`

`// 重試機制`

`**private** Tuple2<TradeVO, WalletVO> retryEarn(EarnDTO earnDTO) {`

`Tuple2<TradeVO, WalletVO> res = self. realEarn(earnDTO);`

`**while** (Objects._isNull_(res)) { // 若是修改失敗進行重試`

`res = realEarn(earnDTO);`

`}`

`**return** res;`

`}`

`// 實際的修改`

`**private** Tuple2<TradeVO, WalletVO> realEarn(EarnDTO earnDTO) {`

`// 問題/problem`

`// 這一行在事物中, 老是會去讀取在當前事物空間中的數據`

`// 重試的機制下, CAS版本號老是不會被更新, 因此會無限制次數的更新, 直到超時, 或者鎖等待超時`

`Wallet wallet = **baseMapper**.selectOne(QueryUtils._eq_(Wallet::getUserId, userId))`

`(update(wallet)){`

`return new Tuple2(); // 修改爲功返回修改信息`

`}`

`**return** **null**; // 修改失敗返回null`

`}`

解決辦法

解決思路

既然找到緣由了, 那麼從緣由入手, 讓每次查詢的時候, 數據的版本號可以進行更新就行了sql

在事物以外進行重試

在​earn()​方法外進行重試, 而不是該方法內部, 每次重試的時候就是開啓一個新的事物, 那麼就不會出現數據版本號沒法跟新的問題了數據庫

`[@Override](https://my.oschina.net/u/1162528)`

`@Transactional(rollbackFor = Exception.**class**) // 開始事物`

`**public** Tuple2<TradeVO, WalletVO> earn(EarnDTO earnDTO) {`

`**return** realEarn(earnDTO);`

`}`

`// 實際的修改`

`**private** Tuple2<TradeVO, WalletVO> realEarn(EarnDTO earnDTO) {`

`// 問題/problem`

`// 這一行在事物中, 老是會去讀取在當前事物空間中的數據`

`// 重試的機制下, CAS版本號老是不會被更新, 因此會無限制次數的更新, 直到超時, 或者鎖等待超時`

`Wallet wallet = **baseMapper**.selectOne(QueryUtils._eq_(Wallet::getUserId, userId))`

`(update(wallet)){`

`return new Tuple2(); // 修改爲功返回修改信息`

`}`

`**return** **null**; // 修改失敗返回null`

`}`

`// 重試機制`

`// 在事物以外進行重試`

`**public** Tuple2<TradeVO, WalletVO> retryEarn(EarnDTO earnDTO) {`

`Tuple2<TradeVO, WalletVO> res = earn(earnDTO)`

`**while** (Objects._isNull_(res)) { // 若是修改失敗進行重試`

`res = earn(earnDTO);`

`}`

`**return** res;`

`}`

把查詢SQL語句隔離事物以外

利用@Transaction的事物傳播屬性​propagation = Propagation.**REQUIRES_NEW**​把查詢SQL語句隔離以外, 每次查詢的時候, 就會拿到最新的數據版本號, 這樣就不會致使無限次重試了app

須要注意代碼內部用this.方法進行調用是不會生效的, 由於數據事物是編織在AOP中的, 必須Spring代理對象才能使得​**REQUIRES_NEW**​屬性生效ide

`@Override`

`@Transactional(rollbackFor = Exception.**class)**`

`**public** Tuple2<TradeVO, WalletVO> earn(EarnDTO earnDTO) {`

`**return** retryEarn(earnDTO);`

`}`

`**private** Tuple2<TradeVO, WalletVO> retryEarn(EarnDTO earnDTO) {`

`Tuple2<TradeVO, WalletVO> res = self. realEarn(earnDTO);`

`**while** (Objects._isNull_(res)) {`

`res = realEarn(earnDTO);`

`}`

`**return** res;`

`}`

`**private** Tuple2<TradeVO, WalletVO> realEarn(EarnDTO earnDTO) {`

`// 注意這裏是self. 而不是 this.`

`Wallet wallet = self.getWallet();`

`(update(wallet)){`

`return new Tuple2();`

`}`

`**return** **null**;`

`}`

`// 開啓一個新的獨立事物去讀取數據, 避免數據庫產生幻讀`

`@Override`

`@Transactional(readOnly = **true**, propagation = Propagation.**_REQUIRES_NEW_**)`

`**public** Wallet getWallet(String userId) {`

`**return** **baseMapper**.selectOne(QueryUtils._eq_(Wallet::getUserId, userId));`

`}`
相關文章
相關標籤/搜索