一把“樂觀鎖”輕松搞定高并發(fā)下的冪等性問題(附視頻教程)
點(diǎn)擊上方藍(lán)色字體,選擇“標(biāo)星公眾號”
優(yōu)質(zhì)文章,第一時間送達(dá)
66套java從入門到精通實(shí)戰(zhàn)課程分享
什么是冪等性?
高并發(fā)下的冪等性問題
這里以兩個實(shí)例來看下高并發(fā)下的冪等性問題;
一,購票實(shí)例
購票實(shí)現(xiàn)流程如下:
step1:查詢是否有票,有票的話,繼續(xù)下一步,否則提示無票,結(jié)束;
step2:從用戶賬戶扣除票款;
step3:余票減一操作;
這里的話,正常情況沒問題,但是比如用戶連續(xù)多點(diǎn)了幾次,或者網(wǎng)絡(luò)問題導(dǎo)致的再或者多人同時購買的時候的并發(fā)情況下,step1步驟會有兩個或者多個線程同時進(jìn)入,這時候判斷都是有票的,然后繼續(xù)進(jìn)入step2,step3,這時候,就可能會出現(xiàn)余票負(fù)數(shù),多賣的情況;
二,充值實(shí)例
充值實(shí)現(xiàn)流程如下:
step1:用戶輸入充值金額,請求后端業(yè)務(wù)系統(tǒng);
step2:后端生成訂單,訂單狀態(tài)是未支付,然后再請求第三方支付接口;
step3:用戶端確認(rèn)支付;
step4:第三方支付通過我方提供的回調(diào)接口異步通知支付結(jié)果;
具體step4 demo代碼如下:
System.out.println("查詢訂單");
Order order = orderMapper.getByOrderId(orderId); // 根據(jù)訂單id獲取訂單
if(order.getStatus()==0){ // 假如是未支付狀態(tài)
??System.out.println("未支付狀態(tài)");
??order.setStatus(1); // 設(shè)置支付成功狀態(tài)
??System.out.println("更新支付狀態(tài)...");
??orderMapper.update(order); // 更新支付狀態(tài)
??System.out.println("賬戶充值...");
??userAccountMapper.addAmount(order.getAmount(),userAccount.getUserId()); // 賬戶充值
??System.out.println("充值完畢...");
??return?true;
}else{ // 已經(jīng)支付成功,訂單已處理
??System.out.println("發(fā)現(xiàn)訂單已處理");
??return?true;
}這個第四步是有缺陷的,假如第三方支付系統(tǒng)問題或者網(wǎng)絡(luò)問題,有多個線程同時執(zhí)行進(jìn)入?
Order?order = orderMapper.getByOrderId(orderId);?根據(jù)訂單id查詢訂單信息,發(fā)現(xiàn)status狀態(tài)都是未支付,所以都進(jìn)入if里面,這時候就出現(xiàn)了賬戶重復(fù)充值的情況;
冪等性問題總結(jié)
冪等性問題解決方案
關(guān)于冪等性問題的解決方案,業(yè)界提供了很多解決方案,如單機(jī)系統(tǒng)的Java 同步鎖,樂觀鎖,悲觀鎖,分布式鎖,唯一性索引,token機(jī)制防止頁面重復(fù)提交等,每種方案各有利弊;不過主流的話,還是樂觀鎖和分布式鎖這兩個方案;
Java同步鎖方案
我們可以使用synchronized同步鎖,把查詢狀態(tài)的代碼和更新的代碼放一個同步鎖內(nèi),這樣同一時刻只能有一個線程進(jìn)入執(zhí)行,等執(zhí)行完其他線程才能進(jìn)入,這樣能解決冪等性問題,但是假如同步塊里面的業(yè)務(wù)代碼執(zhí)行時間比較長,這樣會嚴(yán)重影響用戶體驗,和系統(tǒng)的吞吐量。所以不是最佳方案;
悲觀鎖方案
悲觀鎖(Pessimistic Lock),顧名思義,就是很悲觀,每次去拿數(shù)據(jù)的時候都認(rèn)為別人會修改,所以每次在拿數(shù)據(jù)的時候都會上鎖,這樣別人想拿這個數(shù)據(jù)就會block直到它拿到鎖。
悲觀鎖:假定會發(fā)生并發(fā)沖突,屏蔽一切可能違反數(shù)據(jù)完整性的操作。
Java synchronized 就屬于悲觀鎖的一種實(shí)現(xiàn),每次線程要修改數(shù)據(jù)時都先獲得鎖,保證同一時刻只有一個線程能操作數(shù)據(jù),其他線程則會被block。
數(shù)據(jù)庫的悲觀鎖通過 for update 實(shí)現(xiàn)的;
select?* from?t_order where?orderId=#{orderId} for?update悲觀鎖使用時一般伴隨事務(wù)一起使用,數(shù)據(jù)鎖定時間可能會很長,影響用戶體驗和系統(tǒng)吞吐量,所以一般也不采用。
樂觀鎖方案
樂觀鎖(Optimistic Lock),顧名思義,就是很樂觀,每次去拿數(shù)據(jù)的時候都認(rèn)為別人不會修改,所以不會上鎖,但是在提交更新的時候會判斷一下在此期間別人有沒有去更新這個數(shù)據(jù)。樂觀鎖適用于讀多寫少的應(yīng)用場景,這樣可以提高吞吐量。
樂觀鎖:假設(shè)不會發(fā)生并發(fā)沖突,只在提交操作時檢查是否違反數(shù)據(jù)完整性。
樂觀鎖一般來說有以下2種方式:?
1. 使用數(shù)據(jù)版本(Version)記錄機(jī)制實(shí)現(xiàn),這是樂觀鎖最常用的一種實(shí)現(xiàn)方式。何謂數(shù)據(jù)版本?即為數(shù)據(jù)增加一個版本標(biāo)識,一般是通過為數(shù)據(jù)庫表增加一個數(shù)字類型的 “version” 字段來實(shí)現(xiàn)。當(dāng)讀取數(shù)據(jù)時,將version字段的值一同讀出,數(shù)據(jù)每更新一次,對此version值加一。當(dāng)我們提交更新的時候,判斷數(shù)據(jù)庫表對應(yīng)記錄的當(dāng)前版本信息與第一次取出來的version值進(jìn)行比對,如果數(shù)據(jù)庫表當(dāng)前版本號與第一次取出來的version值相等,則予以更新,否則認(rèn)為是過期數(shù)據(jù)。?
2. 使用時間戳(timestamp)。樂觀鎖定的第二種實(shí)現(xiàn)方式和第一種差不多,同樣是在需要樂觀鎖控制的table中增加一個字段,名稱無所謂,字段類型使用時間戳(timestamp), 和上面的version類似,也是在更新提交的時候檢查當(dāng)前數(shù)據(jù)庫中數(shù)據(jù)的時間戳和自己更新前取到的時間戳進(jìn)行對比,如果一致則OK,否則就是版本沖突。
樂觀鎖方案在不影響系統(tǒng)性能的情況下,解決了高并發(fā)冪等性問題,所以被得到廣泛使用。唯一的缺點(diǎn)就是對代碼具有入侵性。
分布式鎖
對于分布式系統(tǒng),多個系統(tǒng)獨(dú)立運(yùn)行,所以同步鎖肯定是不行的;對于分布式系統(tǒng),可以用樂觀鎖或者分布式鎖來解決冪等性問題;
具體方案有:
1. 基于緩存(Redis等)實(shí)現(xiàn)分布式鎖;
2. 基于Zookeeper實(shí)現(xiàn)分布式鎖;
(備注:下期我們會提供具體實(shí)現(xiàn)方案的視頻教程,感謝關(guān)注)
基于“樂觀鎖”解決冪等性視頻教程
感謝各位兄弟姐妹關(guān)注,鋒哥為了大伙能更深刻的掌握“樂觀鎖”解決冪等性問題,專門錄制了一期視頻教程。主要以賬戶充值為例,采用IDEA開發(fā)工具,數(shù)據(jù)庫Mysql5.7,demo基于springboot+mybatis架構(gòu),用JMeter測試工具模擬,高并發(fā),來測試出冪等性問題,也就賬戶被重復(fù)充值的場景。然后通過基于狀態(tài)機(jī)version字段的樂觀鎖解決方案,解決冪等性問題,也同時附有完整代碼。
紙上得來終覺淺,絕知此事要躬行。
需要多實(shí)戰(zhàn)練習(xí)和思考。
B站視頻教程在線地址:
https://www.bilibili.com/video/BV1uk4y1m7cP/或者問小鋒老師要也行加小鋒老師微信:java9579 ?備注(樂觀鎖)
長按加鋒哥微信
感謝點(diǎn)贊支持下哈?
