說說悲觀鎖、樂觀鎖、分布式鎖

如何確保一個方法,或者一塊代碼在高并發(fā)情況下,同一時間只能被一個線程執(zhí)行,單體應(yīng)用可以使用并發(fā)處理相關(guān)的 API 進(jìn)行控制,但單體應(yīng)用架構(gòu)演變?yōu)榉植际轿⒎?wù)架構(gòu)后,跨進(jìn)程的實(shí)例部署,顯然就沒辦法通過應(yīng)用層鎖的機(jī)制來控制并發(fā)了。那么鎖都有哪些類型,為什么要使用鎖,鎖的使用場景有哪些?今天我們來聊一聊高并發(fā)場景下鎖的使用技巧。
鎖類別
不同的應(yīng)用場景對鎖的要求各不相同,我們先來看下鎖都有哪些類別,這些鎖之間有什么區(qū)別。
悲觀鎖(synchronize) Java 中的重量級鎖 synchronize 數(shù)據(jù)庫行鎖 樂觀鎖 Java 中的輕量級鎖 volatile 和 CAS 數(shù)據(jù)庫版本號 分布式鎖(Redis鎖)
樂觀鎖
就好比說是你是一個生活態(tài)度樂觀積極向上的人,總是往最好的情況去想,比如你每次去獲取共享數(shù)據(jù)的時候會認(rèn)為別人不會修改,所以不會上鎖,但是在更新的時候你會判斷這期間有沒有人去更新這個數(shù)據(jù)。
樂觀鎖使用在前,判斷在后。我們看下偽代碼:
reduce()
{
????select?total_amount?from?table_1
????if(total_amount?{
??????????return?failed.??
????}??
????//其他業(yè)務(wù)邏輯
????update?total_amount?=?total_amount?-?amount?where?total_amount?>?amount;?}
數(shù)據(jù)庫的版本號屬于樂觀鎖; 通過CAS算法實(shí)現(xiàn)的類屬于樂觀鎖。
悲觀鎖
悲觀鎖是怎么理解呢?相對樂觀鎖剛好反過來,總是假設(shè)最壞的情況,假設(shè)你每次拿數(shù)據(jù)的時候會被其他人修改,所以你在每次共享數(shù)據(jù)的時候會對他加一把鎖,等你使用完了再釋放鎖,再給別人使用數(shù)據(jù)。
悲觀鎖判斷在前,使用在后。我們也看下偽代碼:
reduce()
{
????//其他業(yè)務(wù)邏輯
????int?num?=?update?total_amount?=?total_amount?-?amount?where?total_amount?>?amount;?
???if(num?==1?){
??????????//業(yè)務(wù)邏輯.??
????}?
}
Java中的的synchronize是重量級鎖 ,屬于悲觀鎖; 數(shù)據(jù)庫行鎖屬于悲觀鎖;
扣減操作案例
這里舉一個非常常見的例子,在高并發(fā)情況下余額扣減,或者類似商品庫存扣減,也可以是資金賬戶的余額扣減??蹨p操作會發(fā)生什么問題呢?很容易可以看到,可能會發(fā)生的問題是扣減導(dǎo)致的超賣,也就是扣減成了負(fù)數(shù)。
舉個例子,比如我的庫存數(shù)據(jù)只有100個。并發(fā)情況下第1筆請求賣出100個,第2批賣出100元,導(dǎo)致當(dāng)前的庫存數(shù)量為負(fù)數(shù)。遇到這種場景應(yīng)該如何破解呢?這里列舉四種方案。
方案1:同步排它鎖
這時候很容易想到最簡單的方案:同步排它鎖(synchronize)。但是排他鎖的缺點(diǎn)很明顯:
其中一個缺點(diǎn)是,線程串行導(dǎo)致的性能問題,性能消耗比較大。 另一個缺點(diǎn)是無法解決分布式部署情況下跨進(jìn)程問題;
方案2:數(shù)據(jù)庫行鎖
第二我們可能會想到,那用數(shù)據(jù)庫行鎖來鎖住這條數(shù)據(jù),這種方案相比排它鎖解決了跨進(jìn)程的問題,但是依然有缺點(diǎn)。
其中一個缺點(diǎn)就是性能問題,在數(shù)據(jù)庫層面會一直阻塞,直到事務(wù)提交,這里也是串行執(zhí)行; 第二個需要注意設(shè)置事務(wù)的隔離級別是Read Committed,否則并發(fā)情況下,另外的事務(wù)無法看到提交的數(shù)據(jù),依然會導(dǎo)致超賣問題; 缺點(diǎn)三是容易打滿數(shù)據(jù)庫連接,如果事務(wù)中有第三方接口交互(存在超時的可能性),會導(dǎo)致這個事務(wù)的連接一直阻塞,打滿數(shù)據(jù)庫連接。 最后一個缺點(diǎn),容易產(chǎn)生交叉死鎖,如果多個業(yè)務(wù)的加鎖控制不好,就會發(fā)生AB兩條記錄的交叉死鎖。
方案3:redis分布式鎖
前面的方案本質(zhì)上是把數(shù)據(jù)庫當(dāng)作分布式鎖來使用,所以同樣的道理,redis,zookeeper都相當(dāng)于數(shù)據(jù)庫的一種鎖,其實(shí)當(dāng)遇到加鎖問題,代碼本身無論是synchronize或者各種lock使用起來都比較復(fù)雜,所以思路是把代碼處理一致性的問難題交給一個能夠幫助你處理一致性的問題的專業(yè)組件,比如數(shù)據(jù)庫,比如redis,比如zookeeper等。
這里我們分析下分布式鎖的優(yōu)缺點(diǎn):
優(yōu)點(diǎn): 可以避免大量對數(shù)據(jù)庫排他鎖的征用,提高系統(tǒng)的響應(yīng)能力; 缺點(diǎn): 設(shè)置鎖和設(shè)置超時時間的原子性; 不設(shè)置超時時間的缺點(diǎn); 服務(wù)宕機(jī)或線程阻塞超時的情況; 超時時間設(shè)置不合理的情況;
加鎖和過期設(shè)置的原子性
redis加鎖的命令setnx,設(shè)置鎖的過期時間是expire,解鎖的命令是del,但是2.6.12之前的版本中,加鎖和設(shè)置鎖過期命令是兩個操作,不具備原子性。如果setnx設(shè)置完key-value之后,還沒有來得及使用expire來設(shè)置過期時間,當(dāng)前線程掛掉了或者線程阻塞,會導(dǎo)致當(dāng)前線程設(shè)置的key一直有效,后續(xù)的線程無法正常使用setnx獲取鎖,導(dǎo)致死鎖。
針對這個問題,redis2.6.12以上的版本增加了可選的參數(shù),可以在加鎖的同時設(shè)置key的過期時間,保證了加鎖和過期操作原子性的。
但是,即使解決了原子性的問題,業(yè)務(wù)上同樣會遇到一些極端的問題,比如分布式環(huán)境下,A獲取到了鎖之后,因?yàn)榫€程A的業(yè)務(wù)代碼耗時過長,導(dǎo)致鎖的超時時間,鎖自動失效。后續(xù)線程B就意外的持有了鎖,之后線程A再次恢復(fù)執(zhí)行,直接用del命令釋放鎖,這樣就錯誤的將線程B同樣Key的鎖誤刪除了。代碼耗時過長還是比較常見的場景,假如你的代碼中有外部通訊接口調(diào)用,就容易產(chǎn)生這樣的場景。
設(shè)置合理的時長
剛才講到的線程超時阻塞的情況,那么如果不設(shè)置時長呢,當(dāng)然也不行,如果線程持有鎖的過程中突然服務(wù)宕機(jī)了,這樣鎖就永遠(yuǎn)無法失效了。同樣的也存在鎖超時時間設(shè)置是否合理的問題,如果設(shè)置所持有時間過長會影響性能,如果設(shè)置時間過短,有可能業(yè)務(wù)阻塞沒有處理完成,是否可以合理的設(shè)置鎖的時間?
續(xù)命鎖
這是一個很不容易解決的問題,不過有一個辦法能解決這個問題,那就是續(xù)命鎖,我們可以先給鎖設(shè)置一個超時時間,然后啟動一個守護(hù)線程,讓守護(hù)線程在一段時間之后重新去設(shè)置這個鎖的超時時間,續(xù)命鎖的實(shí)現(xiàn)過程就是寫一個守護(hù)線程,然后去判斷對象鎖的情況,快失效的時候,再次進(jìn)行重新加鎖,但是一定要判斷鎖的對象是同一個,不能亂續(xù)。
同樣,主線程業(yè)務(wù)執(zhí)行完了,守護(hù)線程也需要銷毀,避免資源浪費(fèi),使用續(xù)命鎖的方案相對比較而言更復(fù)雜,所以如果業(yè)務(wù)比較簡單,可以根據(jù)經(jīng)驗(yàn)類比,合理的設(shè)置鎖的超時時間就行。
方案4:數(shù)據(jù)庫樂觀鎖
數(shù)據(jù)庫樂觀鎖加鎖的一個原則就是盡量想辦法減少鎖的范圍。鎖的范圍越大,性能越差,數(shù)據(jù)庫的鎖就是把鎖的范圍減小到了最小。我們看下面的偽代碼
reduce()
{
????select?total_amount?from?table_1
????if(total_amount?{
??????????return?failed.??
????}??
????//其他業(yè)務(wù)邏輯
????update?total_amount?=?total_amount?-?amount;??
}
我們可以看到修改前的代碼是沒有where條件的。修改后,再加where條件判斷:總庫存大于將被扣減的庫存。
update?total_amount?=?total_amount?-?amount?where?total_amount?>?amount
如果更新條數(shù)返回0,說明在執(zhí)行過程中被其他線程搶先執(zhí)行扣減,并且避免了扣減為負(fù)數(shù)。
但是這種方案還會涉及一個問題,如果在之前的update代碼中,以及其他的業(yè)務(wù)邏輯中還有一些其他的數(shù)據(jù)庫寫操作的話,那這部分?jǐn)?shù)據(jù)如何回滾呢?
我的建議是這樣的,你可以選擇下面這兩種寫法:
利用事務(wù)回滾寫法:
我們先給業(yè)務(wù)方法增加事務(wù),方法在扣減庫存影響條數(shù)為零的時候扔出一個異常,這樣對他之前的業(yè)務(wù)代碼也會回滾。
reduce()
{
????select?total_amount?from?table_1
????if(total_amount?{
??????????return?failed.??
????}??
????//其他業(yè)務(wù)邏輯
????int?num?=?update?total_amount?=?total_amount?-?amount?where?total_amount?>?amount;???if(num==0)?throw?Exception;}
第二種寫法
reduce()
{
????//其他業(yè)務(wù)邏輯
????int?num?=?update?total_amount?=?total_amount?-?amount?where?total_amount?>?amount;????if(num?==1?){
??????????//業(yè)務(wù)邏輯.??
????}??else{????throw?Exception;??}
}
首先執(zhí)行update業(yè)務(wù)邏輯,如果執(zhí)行成功了再去執(zhí)行邏輯操作,這種方案是我相對比較建議的方案。在并發(fā)情況下對共享資源扣減操作可以使用這種方法,但是這里需要引出一個問題,比如說萬一其他業(yè)務(wù)邏輯中的業(yè)務(wù),因?yàn)樘厥庠蚴×嗽撛趺崔k呢?比如說在扣減過程中服務(wù)OOM了怎么辦?
我只能說這些非常極端的情況,比如突然宕機(jī)中間數(shù)據(jù)都丟了,這種極少數(shù)的情況下只能人工介入,如果所有的極端情況都考慮到,也不現(xiàn)實(shí)。我們討論的重點(diǎn)是并發(fā)情況下,共享資源的操作如何加鎖的問題。
總結(jié)
最后我來給你總結(jié)一下,如果你可以非常熟練的解決這類問題,第一時間肯定想到的是:數(shù)據(jù)庫版本號解決方案或者分布式鎖的解決方案;但是如果你是一個初學(xué)者,相信你一定會第一時間考慮到Java中提供的同步鎖或者數(shù)據(jù)庫行鎖。
今天討論的目的就是希望把這幾種場景中的鎖放到一個具體的場景中,逐步去對比和分析,讓你能夠更加全面體系的了解使用鎖這個問題的來龍去脈。
來源:www.cnblogs.com/jackyfei/p/12142840.html
版權(quán)申明:內(nèi)容來源網(wǎng)絡(luò),版權(quán)歸原創(chuàng)者所有。除非無法確認(rèn),我們都會標(biāo)明作者及出處,如有侵權(quán)煩請告知,我們會立即刪除并表示歉意。謝謝!

