先說場景:
物品W現(xiàn)在庫存剩余1個, 用戶P1,P2同時購買.則只有1人能購買成功.(前提是不允許超賣)
秒殺也是類似的情況, 只有1件商品,N個用戶同時搶購,只有1人能搶到..
這里不談秒殺設(shè)計,不談使用隊列等使請求串行化,就談下怎么用鎖來保證數(shù)據(jù)正確.
常見的實現(xiàn)方案有以下幾種:
- 代碼同步, 例如使用 synchronized ,lock 等同步方法
-
不查詢,直接更新
update table set surplus = (surplus - buyQuantity) where id = xx and (surplus - buyQuantity) > 0
-
使用CAS,
update table set surplus = aa where id = xx and version = y
-
使用數(shù)據(jù)庫鎖,
select xx for update
- 使用分布式鎖(zookeeper,redis等)
下面就針對這幾種方案來分析下;
1.代碼同步, 例如使用 synchronized ,lock 等同步方法
面試的時候,我經(jīng)常會問這個問題,很大一部分人都會回答用這個方案來實現(xiàn).
偽代碼如下:
publicsynchronizedvoidbuy(StringproductName,IntegerbuyQuantity){
//其他校驗...
//校驗剩余數(shù)量
Productproduct=從數(shù)據(jù)庫查詢出記錄;
if(product.getSurplusreturn"庫存不足";
}
//set新的剩余數(shù)量
product.setSurplus(product.getSurplus()-quantity);
//更新數(shù)據(jù)庫
update(product);
//記錄日志...
//其他業(yè)務(wù)...
}
在方法聲明加上synchronized關(guān)鍵字,實現(xiàn)同步,這樣2個用戶同時購買,到buy方法時候同步執(zhí)行,第2個用戶執(zhí)行的時候,會庫存不足.
嗯.. 看著挺合理的,以前我也是這么干的. 所以現(xiàn)在碰到別人這樣回答,我就會在心里默默的想.小伙子你是沒踩過這坑啊.
先說下這個方案的前提配置:
- 使用spring 聲明式事務(wù)管理
- 事務(wù)傳播機(jī)制使用默認(rèn)的(PROPAGATION_REQUIRED)
- 項目分層為controller-service-dao 3層, 事務(wù)管理在service層
這個方案不可行,主要是因為以下幾點:
1).synchronized 作用范圍是單個jvm實例, 如果做了集群,分布式等,就沒用了
2).synchronized是作用在對象實例上的,如果不是單例,則多個實例間不會同步(這個一般用spring管理bean,默認(rèn)就是單例)
3).單個jvm時,synchronized也不能保證多個數(shù)據(jù)庫事務(wù)的隔離性. 這與代碼中的事務(wù)傳播級別,數(shù)據(jù)庫的事務(wù)隔離級別,加鎖時機(jī)等相關(guān).
3-1).先說隔離級別,常用的是 Read Committed 和 Repeatable Read ,另外2種不常用就不說了
3-1-1)RR(Repeatable Read)級別.mysql默認(rèn)的是RR,事務(wù)開啟后,不會讀取到其他事務(wù)提交的數(shù)據(jù)
根據(jù)前面的前提,我們知道在buy方法時會開啟事務(wù).
假設(shè)現(xiàn)在有線程T1,T2同時執(zhí)行buy方法.假設(shè)T1先執(zhí)行,T2等待.
spring的事務(wù)開啟和提交等是通過aop(代理)實現(xiàn)的,所以執(zhí)行buy方法前,就會開啟事務(wù).
這時候T1,T2是兩個事務(wù),當(dāng)T1執(zhí)行完后,T2執(zhí)行,讀取不到T1提交的數(shù)據(jù),所以會出問題.
3-1-2).RC(Read Committed)級別.事務(wù)開啟后,可以讀取到其他事務(wù)提交的數(shù)據(jù)
看起來這個級別可以解決上面的問題.T2執(zhí)行時,可以讀取到T1提交的結(jié)果.
但是問題是,T2執(zhí)行的時候, T1的事務(wù)提交了嗎?
事務(wù)和鎖的流程如下
- 開啟事務(wù)(aop)
- 加鎖(進(jìn)入synchronized方法)
- 釋放鎖(退出synchronized方法)
- 提交事務(wù)(aop)
可以看出是先釋放鎖,再提交事務(wù).所以T2執(zhí)行查詢,可能還是未讀到T1提交的數(shù)據(jù),還會出問題
3-2).根據(jù)3-1中的問題,發(fā)現(xiàn)主要矛盾是事務(wù)開啟和提交的時機(jī)與加鎖解鎖時機(jī)不一致.有小伙伴們可能就想到了解決方案.
3-2-1).在事務(wù)開啟前加鎖,事務(wù)提交后解鎖.
確實是可以,這相當(dāng)于事務(wù)串行化.拋開性能不談,來談?wù)勗趺磳崿F(xiàn).
如果使用默認(rèn)的事務(wù)傳播機(jī)制,那么要保證事務(wù)開啟前加鎖,事務(wù)提交后解鎖,就需要把加鎖,解鎖放在controller層.
這樣就有個潛在問題,所有操作庫存的方法,都要加鎖,而且要是同一把鎖,寫起來挺累的.
而且這樣還是不能跨jvm.
3-2-2).將查詢庫存,扣減庫存這2步操作,單獨提取個方法,單獨使用事務(wù),并且事務(wù)隔離級別設(shè)置為RC.
這個其實和上面的3-2-1異曲同工,最終都是講加解鎖放在了事務(wù)開啟提交外層.
比較而言優(yōu)點是入口少了. controller不用處理.
缺點除了上面的不能跨jvm,還有就是 單獨的這個方法,需要放到另外的service類中.
因為使用spring,同一個bean的內(nèi)部方法調(diào)用,是不會被再次代理的,所以配置的單獨事務(wù)等需要放到另外的service bean 中
2.不查詢,直接更新
看完第一種方案,有小伙伴就說了. 你說的那么復(fù)雜,那么多問題,不就是因為查詢的數(shù)據(jù)不是最新的嗎?
我們不查詢,直接更新不就行啦.
偽代碼如下:
publicsynchronizedvoidbuy(StringproductName,IntegerbuyQuantity){
//其他校驗...
int影響行數(shù)=updatetablesetsurplus=(surplus-buyQuantity)whereid=1;
if(result0){
return"庫存不足";
}
//記錄日志...
//其他業(yè)務(wù)...
}
測試后發(fā)現(xiàn)庫存變成-1了, 繼續(xù)完善下
publicsynchronizedvoidbuy(StringproductName,IntegerbuyQuantity){
//其他校驗...
int影響行數(shù)=updatetablesetsurplus=(surplus-buyQuantity)whereid=1and(surplus-buyQuantity)>0;
if(result0){
return"庫存不足";
}
//記錄日志...
//其他業(yè)務(wù)...
}
測試后,功能OK;
這樣確實可以實現(xiàn),不過有一些其他問題:
- 不具備通用性,例如add操作
- 庫存操作一般要記錄操作前后的數(shù)量等,這樣沒法記錄
- 其他...
但是根據(jù)這個方案,可以引出方案3.
3.使用CAS, update table set surplus = aa where id = xx and yy = y
CAS是指compare/check and swap/set 意思都差不多,不必太糾結(jié)是哪個單詞
我們將上面的sql修改一下:
int影響行數(shù)=updatetablesetsurplus=newQuantitywhereid=1andsurplus=oldQuantity;
這樣,線程T1執(zhí)行完后,線程T2去更新,影響行數(shù)=0,則說明數(shù)據(jù)被更新, 重新查詢判斷執(zhí)行.偽代碼如下:
publicvoidbuy(StringproductName,IntegerbuyQuantity){
//其他校驗...
Productproduct=getByDB(productName);
int影響行數(shù)=updatetablesetsurplus=(surplus-buyQuantity)whereid=1andsurplus=查詢的剩余數(shù)量;
while(result==0){
product=getByDB(productName);
if(查詢的剩余數(shù)量>buyQuantity){
影響行數(shù)=updatetablesetsurplus=(surplus-buyQuantity)whereid=1andsurplus=查詢的剩余數(shù)量;
}else{
return"庫存不足";
}
}
//記錄日志...
//其他業(yè)務(wù)...
}
看到重新查詢幾個字,小伙伴們應(yīng)該就又想到事務(wù)隔離級別問題了.
沒錯,所以上面代碼中的getByDB方法,必須單獨事務(wù)(注意同一個bean內(nèi)單獨事務(wù)不生效哦),而且數(shù)據(jù)庫的事務(wù)隔離級別必須是RC,
否則上面的代碼就會是死循環(huán)了.
上面的方案,可能會出現(xiàn)一個CAS中經(jīng)典問題. ABA的問題.
ABA是指:
- 線程T1 查詢,庫存剩余 100
- 線程T2 查詢,庫存剩余 100
-
線程T1 執(zhí)行 sub
update t set surplus = 90 where id = x and surplus = 100;
- 線程T3 查詢, 庫存剩余 90
-
線程T3 執(zhí)行add
update t set surplus = 100 where id = x and surplus = 90;
-
線程T2 執(zhí)行sub
update t set surplus = 90 where id = x and surplus = 100;
這里線程T2執(zhí)行的時候,庫存的100已經(jīng)不是查詢到的100了,但是對于這個業(yè)務(wù)是不影響的.
一般的設(shè)計中CAS會使用version來控制.
updatetsetsurplus=90,version=version+1whereid=xandversion=oldVersion;
這樣,每次更新version在原基礎(chǔ)上+1,就可以了.
使用CAS要注意幾點,
- 失敗重試次數(shù),是否需要限制
- 失敗重試對用戶是透明的
4.使用數(shù)據(jù)庫鎖, select xx for update
方案3種的cas,是樂觀鎖的實現(xiàn), 而select for udpate 則是悲觀鎖. 在查詢數(shù)據(jù)的時候,就將數(shù)據(jù)鎖住.
偽代碼如下:
publicvoidbuy(StringproductName,IntegerbuyQuantity){
//其他校驗...
Productproduct=select*fromtablewherename=productNameforupdate;
if(查詢的剩余數(shù)量>buyQuantity){
影響行數(shù)=updatetablesetsurplus=(surplus-buyQuantity)wherename=productName;
}else{
return"庫存不足";
}
//記錄日志...
//其他業(yè)務(wù)...
}
線程T1 進(jìn)行sub , 查詢庫存剩余 100
線程T2 進(jìn)行sub , 這時候,線程T1事務(wù)還未提交,線程T2阻塞,直到線程T1事務(wù)提交或回滾才能查詢出結(jié)果.
所以線程T2查詢出的一定是最新的數(shù)據(jù).相當(dāng)于事務(wù)串行化了,就解決了數(shù)據(jù)一致性問題.
對于select for update,需要注意的有2點.
-
統(tǒng)一入口:所有庫存操作都需要統(tǒng)一使用 select for update ,這樣才會阻塞, 如果另外一個方法還是普通的select, 是不會被阻塞的
-
加鎖順序:如果有多個鎖,那么加鎖順序要一致,否則會出現(xiàn)死鎖.
5.使用分布式鎖(zookeeper,redis等)
使用分布式鎖,原理和方案1種的synchronized是一樣的.只不過synchronized的flag只有jvm進(jìn)程內(nèi)可見,而分布式鎖的flag則是全局可見.方案4種的select for update
的flag 也是全局可見.
分布式鎖的實現(xiàn)方案有很多:基于redis,基于zookeeper,基于數(shù)據(jù)庫等等.
需要注意,使用分布式鎖和synchronized鎖有同樣的問題,就是鎖和事務(wù)的順序,這個在方案1里面已經(jīng)講過.不再重復(fù).
做個簡單總結(jié):
- 方案1: synchronized等jvm內(nèi)部鎖不適合用來保證數(shù)據(jù)庫數(shù)據(jù)一致性,不能跨jvm
- 方案2: 不具備通用性,不能記錄操作前后日志
- 方案3: 推薦使用.但是如果數(shù)據(jù)競爭激烈,則自動重試次數(shù)會急劇上升,需要注意.
- 方案4: 推薦使用.最簡單的方案,但是如果事務(wù)過大,會有性能問題.操作不當(dāng),會有死鎖問題
- 方案5: 和方案1類似,只是能跨jvm
-
數(shù)據(jù)
+關(guān)注
關(guān)注
8文章
7102瀏覽量
89270 -
源代碼
+關(guān)注
關(guān)注
96文章
2946瀏覽量
66830
原文標(biāo)題:實踐角度,談?wù)剮齑婵蹨p和鎖
文章出處:【微信號:AndroidPush,微信公眾號:Android編程精選】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論