<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>

          《我想進大廠》之分布式鎖奪命連環(huán)9問 | 大理版人在囧途

          共 4702字,需瀏覽 10分鐘

           ·

          2021-03-10 13:15

          開個頭,這是篇技術文章,但是昨天一天太惡心了,忍不住還是簡單說下昨天的事情。

          昨天早上11點飛大理,結果9點鐘要出門的時候發(fā)現(xiàn)密碼鎖壞了,不用密碼都能打開,一邊司機師傅在催著走,一邊連忙打電話給房東和客服找人維修,這是第一。

          然后飛機晚點,11點20飛到4點鐘才要落地,下降的過程那叫一個顛簸,我以為都要沒了,這也是第一次暈飛機,簡直快吐了,這是第二。

          然后快4點了,飛機總算快要降落了,輪子都快著地了,結果愣是拔起來又起飛了,最后知道是大理8級大風,機長不敢落地。。。這是第三。

          最后通知起飛不知道什么時候,要等大理那邊通知,沒有辦法,我們只好下飛機轉高鐵,急急忙忙的一路轉,總算趕上了最后7點前的高鐵,否則就要等到9點以后了,最后一路周轉,9點多總算到了酒店,好在酒店還算行,沒有讓我太過于失望。

          這一天搞下來,整個一人在囧途,太累了。好吧,廢話就這么多,文章開始。

          說說分布式鎖吧?

          對于一個單機的系統(tǒng),我們可以通過synchronized或者ReentrantLock等這些常規(guī)的加鎖方式來實現(xiàn),然而對于一個分布式集群的系統(tǒng)而言,單純的本地鎖已經無法解決問題,所以就需要用到分布式鎖了,通常我們都會引入三方組件或者服務來解決這個問題,比如數(shù)據(jù)庫、Redis、Zookeeper等。

          通常來說,分布式鎖要保證互斥性、不死鎖、可重入等特點。

          互斥性指的是對于同一個資源,任意時刻,都只有一個客戶端能持有鎖。

          不死鎖指的是必須要有鎖超時這種機制,保證在出現(xiàn)問題的時候釋放鎖,不會出現(xiàn)死鎖的問題。

          可重入指的是對于同一個線程,可以多次重復加鎖。

          那你分別說說使用數(shù)據(jù)庫、Redis和Zookeeper的實現(xiàn)原理?

          數(shù)據(jù)庫的話可以使用樂觀鎖或者悲觀鎖的實現(xiàn)方式。

          樂觀鎖通常就是數(shù)據(jù)庫中我們會有一個版本號,更新數(shù)據(jù)的時候通過版本號來更新,這樣的話效率會比較高,悲觀鎖則是通過for update的方式,但是會帶來很多問題,因為他是一個行級鎖,高并發(fā)的情況下可能會導致死鎖、客戶端連接超時等問題,一般不推薦使用這種方式。

          Redis是通過set命令來實現(xiàn),在2.6.2版本之前,實現(xiàn)方式可能是這樣:

          setNX命令代表當key不存在時返回成功,否則返回失敗。

          但是這種實現(xiàn)方式把加鎖和設置過期時間的步驟分成兩步,他們并不是原子操作,如果加鎖成功之后程序崩潰、服務宕機等異常情況,導致沒有設置過期時間,那么就會導致死鎖的問題,其他線程永遠都無法獲取這個鎖。

          之后的版本中,Redis提供了原生的set命令,相當于兩命令合二為一,不存在原子性的問題,當然也可以通過lua腳本來解決。

          set命令如下格式:

          key 為分布式鎖的key

          value 為分布式鎖的值,一般為不同的客戶端設置不同的值

          NX 代表如果要設置的key已存在,則取消設置

          EX 代表過期時間為秒,PX則為毫秒,比如上面示例中為10秒過期

          Zookeeper是通過創(chuàng)建臨時順序節(jié)點的方式來實現(xiàn)。

          1. 當需要對資源進行加鎖時,實際上就是在父節(jié)點之下創(chuàng)建一個臨時順序節(jié)點。
          2. 客戶端A來對資源加鎖,首先判斷當前創(chuàng)建的節(jié)點是否為最小節(jié)點,如果是,那么加鎖成功,后續(xù)加鎖線程阻塞等待
          3. 此時,客戶端B也來嘗試加鎖,由于客戶端A已經加鎖成功,所以客戶端B發(fā)現(xiàn)自己的節(jié)點并不是最小節(jié)點,就會去取到上一個節(jié)點,并且對上一節(jié)點注冊監(jiān)聽
          4. 當客戶端A操作完成,釋放鎖的操作就是刪除這個節(jié)點,這樣就可以觸發(fā)監(jiān)聽事件,客戶端B就會得到通知,同樣,客戶端B判斷自己是否為最小節(jié)點,如果是,那么則加鎖成功

          你說改為set命令之后就解決了問題?那么還會不會有其他的問題呢?

          雖然set解決了原子性的問題,但是還是會存在兩個問題。

          鎖超時問題

          比如客戶端A加鎖同時設置超時時間是3秒,結果3s之后程序邏輯還沒有執(zhí)行完成,鎖已經釋放。客戶端B此時也來嘗試加鎖,那么客戶端B也會加鎖成功。

          這樣的話,就導致了并發(fā)的問題,如果代碼冪等性沒有處理好,就會導致問題產生。

          鎖誤刪除

          還是類似的問題,客戶端A加鎖同時設置超時時間3秒,結果3s之后程序邏輯還沒有執(zhí)行完成,鎖已經釋放。客戶端B此時也來嘗試加鎖,這時客戶端A代碼執(zhí)行完成,執(zhí)行釋放鎖,結果釋放了客戶端B的鎖。

          那上面兩個問題你有什么好的解決方案嗎?

          鎖超時

          這個有兩個解決方案。

          1. 針對鎖超時的問題,我們可以根據(jù)平時業(yè)務執(zhí)行時間做大致的評估,然后根據(jù)評估的時間設置一個較為合理的超時時間,這樣能一大部分程度上避免問題。
          2. 自動續(xù)租,通過其他的線程為將要過期的鎖延長持有時間

          鎖誤刪除

          每個客戶端的鎖只能自己解鎖,一般我們可以在使用set命令的時候生成隨機的value,解鎖使用lua腳本判斷當前鎖是否自己持有的,是自己的鎖才能釋放。

          #加鎖
          SET key random_value NX EX 10
          #解鎖
          if redis.call("get",KEYS[1]) == ARGV[1] then
              return redis.call("del",KEYS[1])
          else
              return 0
          end

          了解RedLock算法嗎?

          因為在Redis的主從架構下,主從同步是異步的,如果在Master節(jié)點加鎖成功后,指令還沒有同步到Slave節(jié)點,此時Master掛掉,Slave被提升為Master,新的Master上并沒有鎖的數(shù)據(jù),其他的客戶端仍然可以加鎖成功。

          對于這種問題,Redis作者提出了RedLock紅鎖的概念。

          RedLock的理念下需要至少2個Master節(jié)點,多個Master節(jié)點之間完全互相獨立,彼此之間不存在主從同步和數(shù)據(jù)復制。

          主要步驟如下:

          1. 獲取當前Unix時間
          2. 按照順序依次嘗試從多個節(jié)點鎖,如果獲取鎖的時間小于超時時間,并且超過半數(shù)的節(jié)點獲取成功,那么加鎖成功。這樣做的目的就是為了避免某些節(jié)點已經宕機的情況下,客戶端還在一直等待響應結果。舉個例子,假設現(xiàn)在有5個節(jié)點,過期時間=100ms,第一個節(jié)點獲取鎖花費10ms,第二個節(jié)點花費20ms,第三個節(jié)點花費30ms,那么最后鎖的過期時間就是100-(10+20+30),這樣就是加鎖成功,反之如果最后時間<0,那么加鎖失敗
          3. 如果加鎖失敗,那么要釋放所有節(jié)點上的鎖

          那么RedLock有什么問題嗎?

          其實RedLock存在不少問題,所以現(xiàn)在其實一般不推薦使用這種方式,而是推薦使用Redission的方案,他的問題主要如下幾點。

          性能、資源

          因為需要對多個節(jié)點分別加鎖和解鎖,而一般分布式鎖的應用場景都是在高并發(fā)的情況下,所以耗時較長,對性能有一定的影響。此外因為需要多個節(jié)點,使用的資源也比較多,簡單來說就是費錢。

          節(jié)點崩潰重啟

          比如有1~5號五個節(jié)點,并且沒有開啟持久化,客戶端A在1,2,3號節(jié)點加鎖成功,此時3號節(jié)點崩潰宕機后發(fā)生重啟,就丟失了加鎖信息,客戶端B在3,4,5號節(jié)點加鎖成功。

          那么,兩個客戶端A\B同時獲取到了同一個鎖,問題產生了,怎么解決?

          1. Redis作者建議的方式就是延時重啟,比如3號節(jié)點宕機之后不要立刻重啟,而是等待一段時間后再重啟,這個時間必須大于鎖的有效時間,也就是鎖失效后再重啟,這種人為干預的措施真正實施起來就比較困難了
          2. 第二個方案那么就是開啟持久化,但是這樣對性能又造成了影響。比如如果開啟AOF默認每秒一次刷盤,那么最多丟失一秒的數(shù)據(jù),如果想完全不丟失的話就對性能造成較大的影響。

          GC、網(wǎng)絡延遲

          對于RedLock,Martin Kleppmann提出了很多質疑,我就只舉這樣一個GC或者網(wǎng)絡導致的例子。(這個問題比較多,我就不一一舉例了,心里有一個概念就行了,文章地址:https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html

          從圖中我們可以看出,client1線獲取到鎖,然后發(fā)生GC停頓,超過了鎖的有效時間導致鎖被釋放,然后鎖被client2拿到,然后兩個客戶端同時拿到鎖在寫數(shù)據(jù),問題產生。

          圖片來自Martin Kleppmann

          時鐘跳躍

          同樣的例子,假設發(fā)生網(wǎng)絡分區(qū),4、5號節(jié)點變?yōu)橐粋€獨立的子網(wǎng),3號節(jié)點發(fā)生始終跳躍(不管人為操作還是同步導致)導致鎖過期,這時候另外的客戶端就可以從3、4、5號節(jié)點加鎖成功,問題又發(fā)生了。

          那你說說有什么好的解決方案嗎?

          上面也提到了,其實比較好的方式是使用Redission,它是一個開源的Java版本的Redis客戶端,無論單機、哨兵、集群環(huán)境都能支持,另外還很好地解決了鎖超時、公平非公平鎖、可重入等問題,也實現(xiàn)了RedLock,同時也是官方推薦的客戶端版本。

          那么Redission實現(xiàn)原理呢?

          加鎖、可重入

          首先,加鎖和解鎖都是通過lua腳本去實現(xiàn)的,這樣做的好處是為了兼容老版本的redis同時保證原子性。

          KEYS[1]為鎖的key,ARGV[2]為鎖的value,格式為uuid+線程ID,ARGV[1]為過期時間。

          主要的加鎖邏輯也比較容易看懂,如果key不存在,通過hash的方式保存,同時設置過期時間,反之如果存在就是+1。

          對應的就是hincrby', KEYS[1], ARGV[2], 1這段命令,對hash結構的鎖重入次數(shù)+1。

          解鎖

          1. 如果key都不存在了,那么就直接返回
          2. 如果key、field不匹配,那么說明不是自己的鎖,不能釋放,返回空
          3. 釋放鎖,重入次數(shù)-1,如果還大于0那么久刷新過期時間,反之那么久刪除鎖

          watchdog

          也叫做看門狗,也就是解決了鎖超時導致的問題,實際上就是一個后臺線程,默認每隔10秒自動延長鎖的過期時間。

          默認的時間就是internalLockLeaseTime / 3internalLockLeaseTime默認為30秒。

          最后,實際生產中對于不同的場景該如何選擇?

          首先,如果對于并發(fā)不高并且比較簡單的場景,通過數(shù)據(jù)庫樂觀鎖或者唯一主鍵的形式就能解決大部分的問題。

          然后,對于Redis實現(xiàn)的分布式鎖來說性能高,自己去實現(xiàn)的話比較麻煩,要解決鎖續(xù)租、lua腳本、可重入等一系列復雜的問題。

          對于單機模式而言,存在單點問題。

          對于主從架構或者哨兵模式,故障轉移會發(fā)生鎖丟失的問題,因此產生了紅鎖,但是紅鎖的問題也比較多,并不推薦使用,推薦的使用方式是用Redission。

          但是,不管選擇哪種方式,本身對于Redis來說不是強一致性的,某些極端場景下還是可能會存在問題。

          對于Zookeeper的實現(xiàn)方式而言,本身就是保證數(shù)據(jù)一致性的,可靠性更高,所以不存在Redis的各種故障轉移帶來的問題,自己實現(xiàn)也比較簡單,但是性能相比Redis稍差。

          不過,實際中我們當然是有啥用啥,老板說用什么就用什么,我才不管那么多。

          ·················END·················



          往期推薦

          關于MVCC,我之前寫錯了,這次我改好了!

          長篇連載(一)你的編程能力從什么時候開始突飛猛進?

          好久沒更新,新年第一篇!

          好了,我攤牌了,B站,牛逼!

          長篇連載,人生30年(二):職場菜鳥被開除

          《我想進大廠》之Zookeeper奪命連環(huán)9問


          瀏覽 29
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  欧美第一页福利 | 成人a片在线免费观看 | 毛片的网址 | 日韩无毛| 三级片在线观看视频网址 |