<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          面試官:悲觀鎖、樂(lè)觀鎖、分布式鎖?都在什么場(chǎng)景下使用?有什么技巧?

          共 3805字,需瀏覽 8分鐘

           ·

          2021-01-19 08:26


          點(diǎn)擊上方?泥瓦匠 關(guān)注我!

          • 鎖類別
          • 樂(lè)觀鎖
          • 悲觀鎖
          • 扣減操作案例
            • 方案1:同步排它鎖
            • 方案2:數(shù)據(jù)庫(kù)行鎖
            • 方案3:redis分布式鎖
            • 方案4:數(shù)據(jù)庫(kù)樂(lè)觀鎖
          • 總結(jié)

          如何確保一個(gè)方法,或者一塊代碼在高并發(fā)情況下,同一時(shí)間只能被一個(gè)線程執(zhí)行,單體應(yīng)用可以使用并發(fā)處理相關(guān)的 API 進(jìn)行控制,但單體應(yīng)用架構(gòu)演變?yōu)榉植际轿⒎?wù)架構(gòu)后,跨進(jìn)程的實(shí)例部署,顯然就沒(méi)辦法通過(guò)應(yīng)用層鎖的機(jī)制來(lái)控制并發(fā)了。

          那么鎖都有哪些類型,為什么要使用鎖,鎖的使用場(chǎng)景有哪些?

          鎖類別

          不同的應(yīng)用場(chǎng)景對(duì)鎖的要求各不相同,我們先來(lái)看下鎖都有哪些類別,這些鎖之間有什么區(qū)別。

          • 悲觀鎖(synchronize)


            • Java 中的重量級(jí)鎖 synchronize
          • 數(shù)據(jù)庫(kù)行鎖

          • 樂(lè)觀鎖


            • Java 中的輕量級(jí)鎖 volatile 和 CAS
          • 數(shù)據(jù)庫(kù)版本號(hào)

          • 分布式鎖(Redis鎖)

          樂(lè)觀鎖

          就好比說(shuō)是你是一個(gè)生活態(tài)度樂(lè)觀積極向上的人,總是往最好的情況去想,比如你每次去獲取共享數(shù)據(jù)的時(shí)候會(huì)認(rèn)為別人不會(huì)修改,所以不會(huì)上鎖,但是在更新的時(shí)候你會(huì)判斷這期間有沒(méi)有人去更新這個(gè)數(shù)據(jù)。

          樂(lè)觀鎖使用在前,判斷在后。我們看下偽代碼:

          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ù)庫(kù)的版本號(hào)屬于樂(lè)觀鎖;
          • 通過(guò)CAS算法實(shí)現(xiàn)的類屬于樂(lè)觀鎖。

          悲觀鎖

          悲觀鎖是怎么理解呢?相對(duì)樂(lè)觀鎖剛好反過(guò)來(lái),總是假設(shè)最壞的情況,假設(shè)你每次拿數(shù)據(jù)的時(shí)候會(huì)被其他人修改,所以你在每次共享數(shù)據(jù)的時(shí)候會(huì)對(duì)他加一把鎖,等你使用完了再釋放鎖,再給別人使用數(shù)據(jù)。

          悲觀鎖判斷在前,使用在后。我們也看下偽代碼:

          reduce()
          {
          ????//其他業(yè)務(wù)邏輯
          ????int?num?=?update?total_amount?=?total_amount?-?amount?where?total_amount?>?amount;
          ???if(num?==1?){
          ??????????//業(yè)務(wù)邏輯.
          ????}
          }
          • Java中的的synchronize是重量級(jí)鎖 ,屬于悲觀鎖;
          • 數(shù)據(jù)庫(kù)行鎖屬于悲觀鎖;

          扣減操作案例

          這里舉一個(gè)非常常見(jiàn)的例子,在高并發(fā)情況下余額扣減,或者類似商品庫(kù)存扣減,也可以是資金賬戶的余額扣減。扣減操作會(huì)發(fā)生什么問(wèn)題呢?很容易可以看到,可能會(huì)發(fā)生的問(wèn)題是扣減導(dǎo)致的超賣?,也就是扣減成了負(fù)數(shù)。

          舉個(gè)例子,比如我的庫(kù)存數(shù)據(jù)只有100個(gè)。并發(fā)情況下第1筆請(qǐng)求賣出100個(gè),第2批賣出100元,導(dǎo)致當(dāng)前的庫(kù)存數(shù)量為負(fù)數(shù)。遇到這種場(chǎng)景應(yīng)該如何破解呢?這里列舉四種方案。

          方案1:同步排它鎖

          這時(shí)候很容易想到最簡(jiǎn)單的方案:同步排它鎖?(synchronize)。但是排他鎖的缺點(diǎn)很明顯:

          • 其中一個(gè)缺點(diǎn)是,線程串行導(dǎo)致的性能問(wèn)題,性能消耗比較大。
          • 另一個(gè)缺點(diǎn)是無(wú)法解決分布式部署情況下跨進(jìn)程問(wèn)題;

          方案2:數(shù)據(jù)庫(kù)行鎖

          第二我們可能會(huì)想到,那用數(shù)據(jù)庫(kù)行鎖來(lái)鎖住這條數(shù)據(jù),這種方案相比排它鎖解決了跨進(jìn)程的問(wèn)題,但是依然有缺點(diǎn)。

          • 其中一個(gè)缺點(diǎn)就是性能問(wèn)題,在數(shù)據(jù)庫(kù)層面會(huì)一直阻塞,直到事務(wù)提交,這里也是串行執(zhí)行;
          • 第二個(gè)需要注意設(shè)置事務(wù)的隔離級(jí)別是Read Committed,否則并發(fā)情況下,另外的事務(wù)無(wú)法看到提交的數(shù)據(jù),依然會(huì)導(dǎo)致超賣問(wèn)題;
          • 缺點(diǎn)三是容易打滿數(shù)據(jù)庫(kù)連接,如果事務(wù)中有第三方接口交互(存在超時(shí)的可能性),會(huì)導(dǎo)致這個(gè)事務(wù)的連接一直阻塞,打滿數(shù)據(jù)庫(kù)連接。
          • 最后一個(gè)缺點(diǎn),容易產(chǎn)生交叉死鎖,如果多個(gè)業(yè)務(wù)的加鎖控制不好,就會(huì)發(fā)生AB兩條記錄的交叉死鎖。

          方案3:redis分布式鎖

          前面的方案本質(zhì)上是把數(shù)據(jù)庫(kù)當(dāng)作分布式鎖來(lái)使用,所以同樣的道理,redis,zookeeper都相當(dāng)于數(shù)據(jù)庫(kù)的一種鎖,其實(shí)當(dāng)遇到加鎖問(wèn)題,代碼本身無(wú)論是synchronize或者各種lock使用起來(lái)都比較復(fù)雜,所以思路是把代碼處理一致性的問(wèn)難題交給一個(gè)能夠幫助你處理一致性的問(wèn)題的專業(yè)組件,比如數(shù)據(jù)庫(kù),比如redis,比如zookeeper等。

          這里我們分析下分布式鎖的優(yōu)缺點(diǎn):

          • 優(yōu)點(diǎn):


            • 可以避免大量對(duì)數(shù)據(jù)庫(kù)排他鎖的征用,提高系統(tǒng)的響應(yīng)能力?;
          • 缺點(diǎn):


            • 設(shè)置鎖和設(shè)置超時(shí)時(shí)間的原子性;
          • 不設(shè)置超時(shí)時(shí)間的缺點(diǎn);

          • 服務(wù)宕機(jī)或線程阻塞超時(shí)的情況;

          • 超時(shí)時(shí)間設(shè)置不合理的情況;

          加鎖和過(guò)期設(shè)置的原子性

          redis加鎖的命令setnx,設(shè)置鎖的過(guò)期時(shí)間是expire,解鎖的命令是del,但是2.6.12之前的版本中,加鎖和設(shè)置鎖過(guò)期命令是兩個(gè)操作,不具備原子性。如果setnx設(shè)置完key-value之后,還沒(méi)有來(lái)得及使用expire來(lái)設(shè)置過(guò)期時(shí)間,當(dāng)前線程掛掉了或者線程阻塞,會(huì)導(dǎo)致當(dāng)前線程設(shè)置的key一直有效,后續(xù)的線程無(wú)法正常使用setnx獲取鎖,導(dǎo)致死鎖。

          針對(duì)這個(gè)問(wèn)題,redis2.6.12以上的版本增加了可選的參數(shù),可以在加鎖的同時(shí)設(shè)置key的過(guò)期時(shí)間,保證了加鎖和過(guò)期操作原子性的。

          但是,即使解決了原子性的問(wèn)題,業(yè)務(wù)上同樣會(huì)遇到一些極端的問(wèn)題,比如分布式環(huán)境下,A獲取到了鎖之后,因?yàn)榫€程A的業(yè)務(wù)代碼耗時(shí)過(guò)長(zhǎng),導(dǎo)致鎖的超時(shí)時(shí)間,鎖自動(dòng)失效。后續(xù)線程B就意外的持有了鎖,之后線程A再次恢復(fù)執(zhí)行,直接用del命令釋放鎖,這樣就錯(cuò)誤的將線程B同樣Key的鎖誤刪除了。代碼耗時(shí)過(guò)長(zhǎng)還是比較常見(jiàn)的場(chǎng)景,假如你的代碼中有外部通訊接口調(diào)用,就容易產(chǎn)生這樣的場(chǎng)景。

          設(shè)置合理的時(shí)長(zhǎng)

          剛才講到的線程超時(shí)阻塞的情況,那么如果不設(shè)置時(shí)長(zhǎng)呢,當(dāng)然也不行,如果線程持有鎖的過(guò)程中突然服務(wù)宕機(jī)了,這樣鎖就永遠(yuǎn)無(wú)法失效了。同樣的也存在鎖超時(shí)時(shí)間設(shè)置是否合理的問(wèn)題,如果設(shè)置所持有時(shí)間過(guò)長(zhǎng)會(huì)影響性能,如果設(shè)置時(shí)間過(guò)短,有可能業(yè)務(wù)阻塞沒(méi)有處理完成,是否可以合理的設(shè)置鎖的時(shí)間?

          續(xù)命鎖

          這是一個(gè)很不容易解決的問(wèn)題,不過(guò)有一個(gè)辦法能解決這個(gè)問(wèn)題,那就是續(xù)命鎖,我們可以先給鎖設(shè)置一個(gè)超時(shí)時(shí)間,然后啟動(dòng)一個(gè)守護(hù)線程,讓守護(hù)線程在一段時(shí)間之后重新去設(shè)置這個(gè)鎖的超時(shí)時(shí)間,續(xù)命鎖的實(shí)現(xiàn)過(guò)程就是寫一個(gè)守護(hù)線程,然后去判斷對(duì)象鎖的情況,快失效的時(shí)候,再次進(jìn)行重新加鎖,但是一定要判斷鎖的對(duì)象是同一個(gè),不能亂續(xù)。

          同樣,主線程業(yè)務(wù)執(zhí)行完了,守護(hù)線程也需要銷毀,避免資源浪費(fèi),使用續(xù)命鎖的方案相對(duì)比較而言更復(fù)雜,所以如果業(yè)務(wù)比較簡(jiǎn)單,可以根據(jù)經(jīng)驗(yàn)類比,合理的設(shè)置鎖的超時(shí)時(shí)間就行。

          方案4:數(shù)據(jù)庫(kù)樂(lè)觀鎖

          數(shù)據(jù)庫(kù)樂(lè)觀鎖加鎖的一個(gè)原則就是盡量想辦法減少鎖的范圍。鎖的范圍越大,性能越差,數(shù)據(jù)庫(kù)的鎖就是把鎖的范圍減小到了最小。我們看下面的偽代碼

          reduce()
          {
          ????select?total_amount?from?table_1
          ????if(total_amount?{
          ??????????return?failed.
          ????}
          ????//其他業(yè)務(wù)邏輯
          ????update?total_amount?=?total_amount?-?amount;
          }

          我們可以看到修改前的代碼是沒(méi)有where條件的。修改后,再加where條件判斷:總庫(kù)存大于將被扣減的庫(kù)存。

          update?total_amount?=?total_amount?-?amount?where?total_amount?>?amount

          如果更新條數(shù)返回0,說(shuō)明在執(zhí)行過(guò)程中被其他線程搶先執(zhí)行扣減,并且避免了扣減為負(fù)數(shù)。

          但是這種方案還會(huì)涉及一個(gè)問(wèn)題,如果在之前的update代碼中,以及其他的業(yè)務(wù)邏輯中還有一些其他的數(shù)據(jù)庫(kù)寫操作的話,那這部分?jǐn)?shù)據(jù)如何回滾呢?

          我的建議是這樣的,你可以選擇下面這兩種寫法:

          • 利用事務(wù)回滾寫法:

          我們先給業(yè)務(wù)方法增加事務(wù),方法在扣減庫(kù)存影響條數(shù)為零的時(shí)候扔出一個(gè)異常,這樣對(duì)他之前的業(yè)務(wù)代碼也會(huì)回滾。

          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í)行邏輯操作,這種方案是我相對(duì)比較建議的方案。在并發(fā)情況下對(duì)共享資源扣減操作可以使用這種方法,但是這里需要引出一個(gè)問(wèn)題,比如說(shuō)萬(wàn)一其他業(yè)務(wù)邏輯中的業(yè)務(wù),因?yàn)樘厥庠蚴×嗽撛趺崔k呢?比如說(shuō)在扣減過(guò)程中服務(wù)OOM了怎么辦?

          我只能說(shuō)這些非常極端的情況,比如突然宕機(jī)中間數(shù)據(jù)都丟了,這種極少數(shù)的情況下只能人工介入,如果所有的極端情況都考慮到,也不現(xiàn)實(shí)。我們討論的重點(diǎn)是并發(fā)情況下,共享資源的操作如何加鎖的問(wèn)題。

          總結(jié)

          最后我來(lái)給你總結(jié)一下,如果你可以非常熟練的解決這類問(wèn)題,第一時(shí)間肯定想到的是:數(shù)據(jù)庫(kù)版本號(hào)解決方案或者分布式鎖的解決方案;但是如果你是一個(gè)初學(xué)者,相信你一定會(huì)第一時(shí)間考慮到Java中提供的同步鎖或者數(shù)據(jù)庫(kù)行鎖。

          ?往期推薦

          CTO 級(jí)別的代碼,絕了!

          分頁(yè)使用 OFFSET 和 LIMIT 會(huì)有什么問(wèn)題?

          掌握這款牛逼的 API 敏捷開(kāi)發(fā)工具,告別加班,遠(yuǎn)離搬磚!

          如何 10 分鐘用 Spring Boot + Vue + Antd + US3 搭建自己的圖床?


          下方二維碼關(guān)注我

          技術(shù)草根堅(jiān)持分享?編程,算法,架構(gòu)

          朋友助力下!點(diǎn)個(gè)在看


          瀏覽 42
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  9毛片| 小早川怜子一区二区三区88Av | 青青草无码在线观看 | 色色88| 五月天婷婷六月丁香91 |