點擊上方「小羅技術筆記」,關注公衆號
java
第一時間送達實用乾貨mysql
原本是不打算寫這個文章可是在一個羣裏面發現又有羣友遇到和我同樣的問題不知道咋辦
spring
知識點
一、併發(勉強) 二、mysql MVCC原理 三、spring 事務機制sql
原由
這個話題是由最近一次對接第三方商城發現的,該商城執行流程很奇特,流程以下:數據庫
一、用戶購買,三方平臺調用本系統積分扣除接口,返回結果給三方。編程
二、三方回調本系統商品兌換接口,是否兌換成功,否單獨調用三方失敗處理接口(有步驟3回調),並返回現有接口結果給三方(有步驟3回調)。服務器
三、三方回調用本系統商品兌換成功/失敗接口(確認三方已經收到消息並處理)微信
ps:步驟2兌換流程 加鎖——>查詢訂單是否存在——>扣積分——>插入訂單——>減庫存——>贈送金幣——>釋放鎖(因爲流程如今不管是否兌換成功都必須保存訂單,因此不能在步驟2方法使用事務回滾)併發
這個流程整體看起來很怪,我也是第一次遇到這樣的,不過即便以爲不合理也得按照人家的來。app
問題
若是仔細看看上面執行流程就會發現步驟2會帶來兩次連續的回調,這個連續回調也引起了本文的問題。在測試兌換失敗場景時我這邊要把扣的積分返還給用戶,操做僞代碼以下:
ServiceImpl:@Transactionalpublic void dealOrderExchangeNotice(....){ RedisLocklock= null; try{ lock=newRedisLock(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()都是同樣的,兩次請求查詢的數據狀態是同樣的了
解決過程
兩個請求都拿到了鎖證實第一個回調請求已經執行完畢了,按道理應該將訂單狀態更新成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,將具體的邏輯與事務處理解耦。聲明式事務管理使業務代碼邏輯不受污染,所以實際使用中聲明式事務用的比較多。
小知識: 一、默認配置下 Spring 只會回滾運行時、未檢查異常(繼承自 RuntimeException 的異常)或者 Error。
二、@Transactional 註解只能應用到 public 方法纔有效。
很明顯我這邊也是採用聲明式事務,Aop自動提交事務是在dealOrderExchangeNotice代碼塊中的方法執行完畢後才執行事務提交工做
ps:在羣裏面討論時有一個羣友說事務提交是在finally執行以前,這個觀點是錯誤的
由於這個還在一個羣裏面被人噴了討論的話題老舊
從上面兩個知識點結合以前看的《Mysql45講》(須要,公衆號回覆‘Mysql45講’),我畫了一個執行圖很清晰的說明了問題所在(不懂千萬不要空想動手畫一畫可能立刻明白了)
最後把上面的加鎖代碼轉到controller層後重試沒有出現多返積分的問題了
Controller:public void dealOrderExchangeNotice(....){ RedisLock lock= null; try{ lock=newRedisLock(bizId); if(lock.lock()) { S.dealOrderExchangeNotice(....); }finally{ if(lock!= null) { lock.unlock(); } } }ServiceImpl:@Transactionalpublic void dealOrderExchangeNotice(....){ try{ //查詢訂單 IntegralShoppingOrder shoppingOrder = selectOne(bizId); //shoppingOrder.getStatus()==1 表明訂單扣積分紅功 能夠返還積分 if(shoppingOrder != null&& shoppingOrder.getStatus() == 1) { //返還積分 //更新訂單狀態爲 4(訂單失敗) }catch(Exception e) { }}
相似像這種寫法也是錯誤的
@Service@Transactionalpubli cclassOrderServiceImpl implements OrderService{ @Override public synchronizedint update(Integer id) { ... ... ... }}
總結
鎖不要加載事務中
因爲本人文筆水平有限,文中的描述可能有些不清晰,可是經過問題的排查讓我體驗到理論結合實際代碼的快樂,理論可能不是很高深、很難懂,可是有時木有結合實際也會出現意想不到的問題。
長按二維碼關注
點個在看再走唄!
本文分享自微信公衆號 - 小羅技術筆記(javaCodeNote)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。