併發下加鎖不當,踩坑了!
阿新 • • 發佈:2021-01-07
> 本來是不打算寫這個文章但是在一個群裡面發現又有群友遇到和我一樣的問題不知道咋辦
![](https://gitee.com/luoluo1995/image/raw/master/部落格圖片/20201126/20201126002.png)
## 知識點
1、併發(勉強)
2、mysql MVCC原理
3、spring 事務機制
## 起因
這個話題是由最近一次對接第三方商城發現的,該商城執行流程很奇特,流程如下:
1、使用者購買,三方平臺呼叫本系統積分扣除介面,返回結果給三方。
2、三方回撥本系統商品兌換介面,是否兌換成功,否單獨呼叫三方失敗處理介面(有步驟3回撥),並返回現有介面結果給三方(有步驟3回撥)。
3、三方回撥用本系統商品兌換成功/失敗介面(確認三方已經收到訊息並處理)
ps:步驟2兌換流程 加鎖——>查詢訂單是否存在——>扣積分——>插入訂單——>減庫存——>贈送金幣——>釋放鎖(由於流程現在無論是否兌換成功都必須儲存訂單,所以不能在步驟2方法使用事務回滾)
這個流程總體看起來很怪,我也是第一次遇到這樣的,不過即使覺得不合理也得按照人家的來。
## 問題
如果仔細看看上面執行流程就會發現步驟2會帶來兩次連續的回撥,這個連續回撥也引發了本文的問題。
在測試兌換失敗場景時我這邊要把扣的積分返還給使用者,操作虛擬碼如下:
```
ServiceImpl:
@Transactional
public void dealOrderExchangeNotice(....){
RedisLock lock = null;
try{
lock=new RedisLock(bizId);
if (lock.lock()) {
//查詢訂單
IntegralShoppingOrder shoppingOrder = selectOne(bizId);
//shoppingOrder.getStatus()==1 代表訂單扣積分成功 可以返還積分
if (shoppingOrder != null && shoppingOrder.getStatus() == 1) {
//返還積分
//更新訂單狀態為 4(訂單失敗)
}
}catch (Exception e) {
}finally {
if (lock != null) {
lock.unlock();
}
}
}
```
如果沒有出現問題看著上面的程式碼感覺沒有啥問題的.....
測試時發現每次都是給使用者返還了兩次積分(相當於花100送200了,這哪了得..),剛開始看上面的程式碼看了好久沒有發現問題,加上log後查詢伺服器日誌發現失敗訂單幾乎在同一時間會收到兩條回撥資訊,
(勉強算作一個高併發吧),兩個請求都拿到了鎖且shoppingOrder的getStatus()都是一樣的,感覺到問題了出現重複讀了.........
![](https://gitee.com/luoluo1995/image/raw/master/部落格圖片/20201126/20201126005.png)
## 解決過程
兩個請求都拿到了鎖證明第一個回撥請求已經執行完畢了,按道理應該將訂單狀態更新成4了第二個請求查詢到的也應該是4,但是還是出現同樣的值說明第二個請求查詢時第一個沒有提交事務。
這樣明確出兩個排查方向 重複讀(mysql MVCC原理)、事務提交(spring 事務機制)。
### mysql MVCC原理
mysql預設事務隔離級別是 RR(Repeatable Read,可重複讀),事務A在讀到一條資料之後,此時事務B對該資料進行了修改並提交,那麼事務A再讀該資料,讀到的還是原來的內容。
MVCC的實現,是通過儲存資料在某個時間點的快照來實現的。也就是說,不管需要執行多長時間,每個事務看到的資料是一致的。根據事務開始的時間不同,每個事物對同一張表,同一時刻看到的資料可能是不一樣的。
由此可以確定第二個請求執行查詢時第一個請求事務沒有提交,兩者的事務版本號是一樣的所以查詢的值是一樣的,因此問題不在資料庫了!
>小知識:
>第一個SELECT執行的時候,當前事務取到了系統版本號n(**並不是begin的時候就生成版本號,而是執行事務內第一個語句時生成**),系統版本號自增為n+1。此後,其他事務的更新操作能取到的系統版本號最小為n+1,所以當前事務再次SELECT將看不見它們的更新。
### spring 事務機制
Spring 事務管理分為程式設計式和宣告式兩種。程式設計式事務指的是通過編碼方式實現事務;宣告式事務基於 AOP,將具體的邏輯與事務處理解耦。
宣告式事務管理使業務程式碼邏輯不受汙染,因此實際使用中宣告式事務用的比較多。
>小知識:
1、預設配置下 Spring 只會回滾執行時、未檢查異常(繼承自 RuntimeException 的異常)或者 Error。
>2、@Transactional 註解只能應用到 public 方法才有效。
很明顯我這邊也是採用宣告式事務,Aop自動提交事務是在dealOrderExchangeNotice程式碼塊中的方法執行完畢後才執行事務提交工作
**ps:在群裡面討論時有一個群友說事務提交是在finally執行之前,這個觀點是錯誤的**
因為這個還在一個群裡面被人噴了討論的話題老舊
![](https://gitee.com/luoluo1995/image/raw/master/部落格圖片/20201126/20201126003.png)
![](https://gitee.com/luoluo1995/image/raw/master/部落格圖片/20201126/20201126004.png)
從上面兩個知識點結合之前看的《Mysql45講》(需要,公眾號回覆‘Mysql45講’),我畫了一個執行圖很清晰的說明了問題所在(不懂千萬不要空想動手畫一畫可能馬上明白了)
![](https://gitee.com/luoluo1995/image/raw/master/部落格圖片/20201126/20201126001.png)
最後把上面的加鎖程式碼轉到controller層後重試沒有出現多返積分的問題了
```
Controller:
public void dealOrderExchangeNotice(....){
RedisLock lock = null;
try{
lock=new RedisLock(bizId);
if (lock.lock()) {
S.dealOrderExchangeNotice(....);
}finally {
if (lock != null) {
lock.unlock();
}
}
}
ServiceImpl:
@Transactional
public void dealOrderExchangeNotice(....){
lock = null;
try{
//查詢訂單
IntegralShoppingOrder shoppingOrder = selectOne(bizId);
//shoppingOrder.getStatus()==1 代表訂單扣積分成功 可以返還積分
if (shoppingOrder != null && shoppingOrder.getStatus() == 1) {
//返還積分
//更新訂單狀態為 4(訂單失敗)
}catch (Exception e) {
}
}
```
類似像這種寫法也是錯誤的
```
@Service
@Transactional
public class OrderServiceImpl implements OrderService {
@Override
public synchronized int update(Integer id) {
...
...
...
}
}
```
## 總結
**鎖不要載入事務中**
由於本人文筆水平有限,文中的描述可能有些不清晰,但是通過問題的排查讓我體驗到理論結合實際程式碼的快樂,理論可能不是很高深、很難懂,但是有時木有結合實際也會出現意想不到的問題。
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200902222924552.GIF#pic_center)