分佈式鎖知識總結

分佈式鎖知識總結

爲什麼要用分佈式鎖?

       在單體架構中,我們可以通過sychronized來保證併發安全,但是在分佈式架構中,sychronized沒用,sychronized只能保證一個進程(JVM)中的線程安全,無法跨進程

分佈式鎖的執行流程

 

分佈式鎖的實現方式

1、redis實現分佈式鎖

         最簡單的redis實現就是通過 redis的setnx(set if not exists)命令來實現 

           RedisTemplate對redis進行了封裝,使用redistemplate就能實現 redis的setnx(set if not exists)命令

                    bool   result  =       stringRedisTemplate.opsForValue().setIfAbsent( "key","value"  );  // setIfAbsent方法就相當於  setnx命令

                                         setIfAbsent方法 返回 true  說明key不存在,插入數據成功

                                         setIfAbsent方法 返回 true  說明key已經存在了,插入數據失敗

                                           

          大概的代碼邏輯就是 

                      bool   result  =       stringRedisTemplate.opsForValue().setIfAbsent( "key","value"  );  // 設置key(相當於競爭鎖   )

                      if(!result){

                            return "key已經存在";

                      }

                      執行業務邏輯代碼(比如扣庫存)

                      stringRedisTemplate.delete("key");  // 刪除key (相當於釋放鎖)

 

想想這裏面可能出現的問題:

1、線程在運行業務代碼的時候出現異常,那麼此時就無法釋放鎖,就會形成死鎖。怎麼辦?   =====》try  finally  ,在finally中執行delete操作

 

2、線程在執行業務代碼的時候,服務器宕機了,此時也無法釋放鎖,也會形成死鎖。怎麼辦?  =====》 給 鎖的key設置一個timeout

                       int timeout = 10;

                      stringRedisTemplate.expire("key",timeout ,TimeUnit.SECONDS);

3、此時加鎖與設置timeout之間是有時間損耗的,也就是說如果線程在加完鎖正準備設置timeout時,此時服務宕機了,timeout就設置不了,就跟上面情況類似了,也會產生死鎖,怎麼辦?   ================》 保證  加鎖與設置timeout 的原子性   ( RedisTemplate 提供了一個重載的setIfAbsent方法來保證原則性,此方法底層就是使用了lua腳本)

                     redis會將lua腳本中所有的命令當作一個原子操作執行

                         int timeout = 10;

                        //  bool   result  =       stringRedisTemplate.opsForValue().setIfAbsent( "key","value"  );

                        //  stringRedisTemplate.expire("key",timeout ,TimeUnit.SECONDS);

                          bool   result  =  stringRedisTemplate.opsForValue().setIfAbsent( "key","value" ,timeout ,TimeUnit.SECONDS);  // 這行代碼就相當於把上面兩行代碼進行了原子性操作

 

4、線程 1 還沒執行完業務代碼,此時鎖timeout了,redis把線程1 加的鎖刪了 ,此時線程 2 過來了加了鎖,然後執行業務代碼,注意! 此時線程 1 和線程 2 可能同時操作共享資源 ,然後線程1 此時執行完了刪鎖,注意! 此時線程 1 刪除的鎖是線程 2 加的鎖,然後此時線程 3過來了加了鎖,然後執行業務代碼 ,注意! 此時線程 2 和線程 3可能同時操作共享資源,然後線程2 此時執行完了刪鎖,注意! 此時線程2 刪除的鎖是線程 3 加的鎖,......往復循環 ,導致鎖永久失效。

 

怎麼辦?=====》給自己加的鎖  加一個標識,這個標識只有自己知道,此時鎖就只能自己釋放,別人就釋放不了了

5、線程 1 還沒執行完業務代碼,此時鎖timeout了,redis把線程1 加的鎖刪了 ,此時線程 2 過來了加了鎖,然後執行業務代碼,注意! 此時線程 1 和線程 2 可能同時操作共享資源 ,然後線程1 此時執行完了業務代碼之後刪鎖,哦吼,我的鎖怎麼沒了,然後就報錯了。怎麼辦?

======》加大timeout的值

=======》但是有個問題,timeout越大  ====== 》 服務宕機之後,死鎖時間越長  =======》其他線程等待的時間越長  ======》用戶的脾氣越大。 怎麼辦?

======》開啓一個異步線程,開啓一個定時任務,每個 1/3 timeout掃描一次,看看當前鎖的key 還在不在,當前線程還在不在,是不是死(宕機 )了,然後線程還在,鎖的key也還在,那麼就給鎖續命,重新設置鎖的key 的 timeout  ,只要線程沒執行完就給鎖的key續命,知道線程執行完自動刪除鎖的key(釋放鎖)

此時就不得不介紹一個神奇的框架了 !!!!redisson框架

redisson框架     ---- 一個 redis java client

redisson框架 中解決了上面,底層就是用的setnx ,lua腳本保證原子性,鎖的key默認timeout 30s ,其他線程自旋 , 用timerTask 定時任務 默認  每隔 1/3 timeout 續命一次。

redisson分佈式鎖實現原理:

 

redisson框架 使用如下:

6、如果redis用了集羣(一主多從),線程 1 的鎖的key剛把鎖的key存入redis中,   redis的master 主節點掛了,鎖的key還沒同步到slave從節點,然後此時slave從節點升爲 master主節點,但是此時這個新的master主節點中沒有線程 1 的鎖的key ,但是此時線程 1 還在執行業務代碼,然後纔是 線程 2 來了,由於此時新的master主節點上沒有鎖的key,所以線程2申請鎖成功,然後此時線程 1  和 線程 2 就可能同時訪問並使用共享資源  。  此時就有問題了呀! 怎麼辦?

=======》①用zookeeper    有延時 ,要同步半數以上的follower才能加鎖成功,所以此時不用擔心leader掛了,follower中沒有鎖的key   ②人工補償    ③redis自己解決 用redlock

  redlock  實現原理

大概就是搞多個對等的redis節點,節點之間沒有依賴(主從依賴),通過 setnx命令 加鎖 ,同時發給每個redis節點,發送的這些節點中要有半數以上的redis節點加鎖成功,纔算認爲線程拿到了鎖,才繼續往下執行業務。(原理跟zookeeper類似  , 犧牲了性能,如果沒超過半數,涉及到鎖回滾問題)

 

7、高併發性下請求到了redis,但是redis是單線程工作模式,在redis中就也不是併發執行,而是串行執行,影響性能。怎麼辦?

====》 用庫存舉例,   將庫存分段存儲,分段加鎖=======》把每段的鎖的key放到redis集羣中,把不同鎖的key放到不同的redis master主節點上

        一個段的庫存不夠減怎麼辦?去減下面段的,合併幾個段一起扣減。

 

2、zookeeper實現分佈式鎖

           

              由於zookeeper上面的臨時節點是唯一的,只會創建一個,而鎖也只有一把,所以它能代表這個鎖,多個客戶端去搶這個鎖,也就是去zookeeper上面創建臨時節點,看誰創建的快,誰第一個創建了這個臨時節點誰就獲取到了鎖。

此時clientA第一個成功創建了臨時節點,就代表clientA已經獲取到了鎖,其他client再去創建這個臨時節點,發現這個節點已經存在了,他們就不能去創建了,就都阻塞了,他們一直監聽這個節點的變化,也就是在監聽這個鎖的變化(通過zookeeper  Watch功能)

clientA完成了它的業務操作 ,結束了與zookeeper的會話  臨時節點被自動刪除,就代表釋放了鎖,此時其他client監聽到了臨時節點被刪除了(鎖釋放了),就都會去競爭鎖,也就是去創建臨時節點。

同一時間有多個客戶端在競爭鎖

======》上面這個實現方式有什麼問題呢 ?想想如果上千個client去監聽這個臨時節點的變化,一旦這個臨時節點變化了,然後此時就是上千個client去競爭這個鎖(羊羣效應 / 驚羣效應),這對於zookeeper的壓力是非常大的,所以這個方案不可行。那怎麼辦呢?

======》使用臨時順序節點  + 監聽   

     使用臨時順序節點創建出來的節點都是有序的,只有最小的節點能拿到鎖,其他的節點監聽比它小的前一個節點。

獲取鎖大概流程:  每個client 分別 創建臨時順序節點,然後誰的節點最小就誰能拿到鎖,其他節點就監聽比自己小的前一個節點

   比如下圖:A B  C D分別創建節點,A最先創建,所以它的節點編號最小,所以它能獲取鎖,然後B監聽A,C監聽B,D監聽C

釋放鎖大概流程:clientA 執行完自己的業務之後結束了與zookeeper的會話,會話一結束,節點會自動刪除(釋放了鎖),同時clientB監聽到了clientA的這個節點被刪除後,會去判斷自己的節點是不是最小的節點,如果是最小的就獲取鎖,如果不是就繼續監聽等待,不做任務處理,clientA節點刪除只對clientB有影響,對後面的 C  D 等節點沒有影響,因爲C只監聽B,B是沒有變化的,所以C不會受到影響,不會變,D只監聽C,C沒有變,D也不會有影響,也不會變.......就解決的羊羣(驚羣)效應。

同一時間只有一個客戶端在競爭鎖

大概流程:  自己看圖   , 直接通過 curator框架就可以實現, curator已經將這些問題解決了,封裝好了,直接用就行。

 

 

3、數據庫實現分佈式鎖

     基於數據庫表(主要原理:數據庫的主鍵不能重複,作爲分佈式鎖的實現)

           0、首先先創建一個tb_lock表,用於記錄當前哪個線程正在使用數據,表裏就一個  業務ID 或者業務名稱  字段 (唯一主鍵),當作鎖

           1、當線程要訪問數據時,會先將要執行的業務的業務id或者業務名稱 ,insert插入tb_lock表中

           2、當插入成功,代表該線程獲得了鎖,即可執行業務邏輯

           3、當其他線程在執行相同業務的時候也會先將要執行業務的  業務id或者業務名稱  插入tb_lock表中,由於主鍵衝突,此時會導致插入失敗,就代表獲取鎖失敗

           4、獲取鎖成功的線程在執行完業務代碼後,在刪除tb_lock表中刪除對應業務的 業務id或者業務名稱 ,代表釋放鎖

想想這樣實現會出現問題嗎?

1、一旦數據庫掛掉,會導致業務系統不可用。   

                        ====》用兩個數據庫,一主一從,數據之前雙向同步。一旦掛掉快速切換到備庫上。

2、這把鎖沒有失效時間,一旦解鎖操作失敗,就會導致鎖記錄一直在數據庫中,其他線程無法再獲得到鎖。

                        ====》做一個異步定時任務,每隔一定時間把數據庫中的超時數據清理一遍。

3、這把鎖只能是非阻塞的,因爲數據的insert操作,一旦插入失敗就會直接報錯。沒有獲得鎖的線程並不會進入排隊隊列,要想再次獲得鎖就要再次觸發獲得鎖操作。

                        ====》用while循環,直到insert成功再返回成功。

4、這把鎖是非重入的,同一個線程在沒有釋放鎖之前無法再次獲得該鎖。因爲數據中數據已經存在了。

                        ====》在數據庫表中加個字段,記錄當前獲得鎖的機器的主機信息和線程信息,那麼下次再獲取鎖的時候先查詢數據庫,如果當前機器的主機信息和線程信息在數據庫可以查到的話,直接把鎖分配給他就可以了。

     基於數據庫表的排它鎖實現(基於MySql的InnoDB引擎)

           1、在執行業務邏輯之前,先通過一個帶有for  update 的查詢語句 拿到排它鎖 

                                                  select * from table where productId = 1 for update

                         行鎖 : 當前連接要執行的帶有 for update 的SQL語句以後,指定了主鍵查詢,代表當前連接鎖定了這條數據(productId = 1)

                                                  select * from table  for update

                         表鎖:當前連接執行帶有for update的SQL語句以後,沒有指定主鍵查詢,那麼會將表進行鎖定,只有當前連接可以對這張表進行操作

           2、獲得排它鎖的線程即可獲得分佈式鎖,執行業務邏輯

           3、執行完業務邏輯之後,再通過JDBC的connection.commit() 方法來釋放鎖

想想這樣實現會出現問題嗎?

1、一旦數據庫掛掉,會導致業務系統不可用。   

                        ====》使用這種方式,此問題不會發生,服務宕機之後數據庫會自己把鎖釋放掉。

2、這把鎖沒有失效時間,一旦解鎖操作失敗,就會導致鎖記錄一直在數據庫中,其他線程無法再獲得到鎖。

                        ====》做一個異步定時任務,每隔一定時間把數據庫中的超時數據清理一遍。

3、這把鎖只能是非阻塞的,因爲數據的insert操作,一旦插入失敗就會直接報錯。沒有獲得鎖的線程並不會進入排隊隊列,要想再次獲得鎖就要再次觸發獲得鎖操作。

                        ====》使用這種方式,此問題不會發生,for update語句會在執行成功後立即返回,在執行失敗時一直處於阻塞狀態,直到成功。

4、這把鎖是非重入的,同一個線程在沒有釋放鎖之前無法再次獲得該鎖。因爲數據中數據已經存在了。

                        ====》在數據庫表中加個字段,記錄當前獲得鎖的機器的主機信息和線程信息,那麼下次再獲取鎖的時候先查詢數據庫,如果當前機器的主機信息和線程信息在數據庫可以查到的話,直接把鎖分配給他就可以了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章