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;` `}`
利用@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));` `}`