作者:謝益培
1 背景
關鍵詞:并發、丟失更新
預收款賬戶表上有個累計抵扣金額的欄位,該欄位的含義是統計商家預收款賬戶上累計用于抵扣結算成功的金額數,更新時機是,賬單結算完成時,更新累計抵扣金額=累計抵扣金額+賬單金額,
2 問題及現象
發現當賬單結算完成時,偶爾會發生累計抵扣金額欄位值更新不準確的現象,
比如,某商家賬戶上累計抵扣金額原本為0元,當發生兩筆分別為10和8的賬單結算完成后,理論上累計抵扣金額應該變為18元,但實際為10元,也就是說,第二次更新把前一次更新內容給覆寫掉了,
3 問題分析
該問題為典型的第二類丟失更新問題,
3.1 概念解釋
事務在并發情況下,常見如下問題:
- 臟讀:一個事務讀取了已被另個一個事務修改但尚未提交的資料,當一個事務正在訪問資料,并且對資料進行了修改,而這種修改還沒有提交到資料庫中;這時另外一個事務也訪問這個資料,然后使用了這個未提交的資料,
- 不可重復讀:在一個事務內,多次讀同一資料,讀到的結果不同,第一個事務還沒有結束時,另外一個事務也訪問該同一資料,那么,在第一個事務中的兩次讀資料之間,由于第二個事務的修改,那么第一個事務兩次讀到的資料可能是不一樣的,這樣就發生了在一個事務內兩次讀到的資料是不一樣的,因此稱為是不可重復讀,
- 幻讀:同一事務中,當同一個查詢執行多次的時候,由于其他事務進行了插入操作并提交事務,導致每次回傳不同的結果集,幻讀是事務非獨立執行時發生的一種現象,例如第一個事務對一個表中的資料進行了修改,這種修改涉及到表的全部資料行,同時,第二個事務也修改了這個表中的資料,這種修改是向表中插入了一行新資料,那么,就會發生操作第一個事務的用戶發現表中還有沒有修改的資料行,就好像發生了幻覺一樣,
- 更新丟失:兩個事務同時更新一行資料,一個事務對資料的更新把另一個事務對資料的更新覆寫了,這是因為系統沒有執行任何的鎖操作,因此并發事務并沒有被隔離開來,
圖1 SQL標準定義了4種資料庫事務隔離級別
第一類丟失更新:A事務撤銷時,把已經提交的B事務的更新資料覆寫了,SQL標準中未對此做定義,所有資料庫都已解決了第一類丟失更新的問題,
圖2 第一類丟失更新
第二類丟失更新:A事務覆寫B事務已經提交的資料,造成B事務所做操作丟失,第二類丟失更新,和不可重復讀本質上是同一類并發問題,通常將它看成不可重復讀的特例,當兩個或多個事務查詢相同的記錄,然后各自基于查詢的結果更新記錄時會造成第二類丟失更新問題,每個事務不知道其它事務的存在,最后一個事務對記錄所做的更改將覆寫其它事務之前對該記錄所做的更改,
圖3 第二類丟失更新
3.2 疑惑點
發生問題的代碼:
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ) public void finishDeductTransaction(String customerCode, String entityCode, String currency, ABTransaction abTransaction) { Account account = getAccount(customerCode, entityCode, currency); BigDecimal newValue = account.getCumulativeDeductionAmount().add(transaction.getTransactionAmount()); account.setCumulativeDeductionAmount(newValue); //持久化 Account update = new Account(); update.setId(account.getId()); update.setCumulativeDeductionAmount(account.getCumulativeDeductionAmount()); accountBalanceInfoMapper.update(update); }
上述代碼中可以看到,該方法已設定事務隔離級別為可重復讀(isolation = Isolation.REPEATABLE_READ,也是MySQL的默認隔離級別),按照之前對隔離級別規范的理解,可重復讀級別是應該能夠避免第二類更新丟失的問題的,但為啥還是發生了呢?!于是上網查閱相關資料,得到的結論是:MySQL資料庫,設定事務隔離級別為可重復讀無法避免發生“第二類丟失更新”問題,從這個案例中也得到一個教訓就是,規范標準和所選的產品(組件)實際實作情況,兩者需要同時考慮,對于邊緣性或存在爭議的規范內容要盡可能避免直接使用,最好通過其他機制來保證,
4 解決方案
以下整理了針對該問題的常見解決方案并按解決思路進行了分類,
4.1 依賴資料庫的思路
方法1:
將事務隔離級別改為串行,能解決但并發性能低,還可能導致大量超時和鎖競爭,
方法2:
調整SQL陳述句,將更新賦值邏輯改為“c=c+x”形式,其中c為要更新的欄位,x為增量值,這種方式能確保累加值不會被覆寫,但這種方式需要額外撰寫特殊的SQL,而且嚴格意義上講,存在業務邏輯泄露到持久層的不規范問題,
4.2 悲觀鎖思路
方法3:
方法執行時增加分布式鎖,來控制同一賬戶同一時刻只有一個執行緒可對其進行操作,效果等同將事務級別改為串行,也是排隊執行,并發性能也低,只是鎖機制不是由資料庫實作了而已,
分布式鎖的實作方式有多種,比如該專案中有封裝好的基于Redis的分布式鎖,其注解的使用方式如下:
@CbbSingle(key = "QF:finishDeductTransaction:customerCode", value = https://www.cnblogs.com/Jcloud/p/{"#{customerCode}"}) public void finishDeductTransaction(String customerCode, String entityCode, String currency, ABTransaction abTransaction) {
方法4:
通過SQL陳述句啟用資料庫排他鎖,例如:select * from table where name=’xxx’ for update,通過此sql查詢到的資料就會被資料庫上排他鎖,因此其他事務就無法對該資料及進行修改了,
該方法比上面兩種在并發性能方面會好一些,但仍然可能存在鎖等待和超時情況發生,
4.3 樂觀鎖思路
方法5:
樂觀鎖的思路是假設并發沖突發生概率較低,開啟事務時先不加鎖,在更新資料時通過版本比對以及判斷影響行數來判斷是否更新成功,
其中,版本概念,可以是要更新記錄的版本號,或者更新時間等,也可以用舊值條件或校驗和等方式;
該方法并發性能最好,但一旦發生并發沖突會導致方法執行失敗,此時就需要搭配額外的重試或自旋邏輯來倍訓,
思路如下:
//先查詢出來要更新的資料 select column1,id,version from table where id=1001; //進行業務邏輯處理 //更新這條資料 update table set column1=xx where id=1001 and version=查詢出來當時的version值 //判斷影響行數 if (records < 1) 更新失敗...
本案例中的問題便是采用這種方法解決的:
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ) public void finishDeductTransaction(String customerCode, String entityCode, String currency, ABTransaction abTransaction) { Account account = getAccount(customerCode, entityCode, currency); BigDecimal newValue = account.getCumulativeDeductionAmount().add(transaction.getTransactionAmount()); account.setCumulativeDeductionAmount(newValue); //持久化 Account update = new Account(); update.setId(account.getId()); update.setVersion(account.getVersion()); update.setCumulativeDeductionAmount(account.getCumulativeDeductionAmount()); int records = accountBalanceInfoMapper.updateByIdAndVersion(update); if (records < 1) { throw new SingleThreadException("更新時資料版本號已發生改變,原因:發生并發事務"); } }
5 總結
- 對于常見并發事務問題,需要將事務隔離級別和鎖機制結合起來一起使用;
- 需要對并發問題從業務場景上進行分析和識別,對于并發沖突少的場景,首選樂觀鎖思路;對于并發沖突高的場景采用悲觀鎖思路;
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/538198.html
標籤:訊息安全
上一篇:5種典型 API 攻擊及預防建議
