三種使用分佈式鎖方案

1、背景單體架構中使用同步訪問解決多線程併發問題,分佈式中須要有其餘方案。redis

2、分佈式鎖的考量算法

  1.能夠保證在分佈式部署的應用集羣中,同一個方法在同一時間只能被一臺機器-上的一個線程執行。
  2.這把鎖要是一把可重入鎖(避免死鎖)
  3.這把鎖最好是一把阻塞鎖(根據業務需求考慮要不要這條)
  4.這把鎖最好是一把公平鎖(根據業務需求考慮要不要這條)
  5.有高可用的獲取鎖和釋放鎖功能
      保證了只有鎖的持有者才能來解鎖,不然任何競爭者都能解鎖
  6.獲取鎖和釋放鎖的性能要好
    7.若是作的好一點,須要有監控的平臺。數據庫

3、分佈式鎖的三種實現方式

  1.基於數據庫實現排他鎖:利用version字段和for update操做獲取鎖。安全


    優勢:易於理解
    問題:
      (1)鎖沒有失效時間,解鎖失敗時(宕機等緣由),其餘線程獲取不到鎖。
      解決:作一個定時任務實現自動釋放鎖。多線程

      (2)鎖屬於非阻塞,由於獲取鎖的是insert操做,一旦獲取失敗就報錯,沒有得到鎖的線程並不會進入排隊隊列,要想再次得到鎖就要再次觸發得到鎖操做。
      解決:搞一個while循環,直到insert成功再返回成功。架構

      (3)不是可重入鎖。
      解決:加入鎖的機器字段,實現同一機器可重複加鎖。
      另外在解鎖時,必須是鎖的持有者來解鎖,其餘競爭者沒法解鎖併發

      (4)因爲是數據庫,對性能要求高的應用不合適用此實現。
      解決:數據庫自己特性決定。
      (5)在 MySQL 數據庫中採用主鍵衝突防重,在大併發狀況下有可能會形成鎖表現象。dom

      解決:比較好的辦法是在程序中生產主鍵進行防重分佈式

      (6)這把鎖是非公平鎖,全部等待鎖的線程憑運氣去爭奪鎖性能

      解決:再建一張中間表,將等待鎖的線程全記錄下來,並根據建立時間排序,只有最早建立的容許獲取鎖。

      (7)考慮到數據庫單點故障,須要實現數據庫的高可用。

      注意:InnoDB 引擎在加鎖的時候,只有經過索引進行檢索的時候纔會使用行級鎖,不然會使用表級鎖

    另外存在問題
    (1)行級鎖並不必定靠譜:雖然咱們對方法字段名使用了惟一索引,而且顯示使用 for update 來使用行級鎖。
            可是,MySQL 會對查詢進行優化,即使在條件中使用了索引字段,可是否使用索引來檢索數據是由 MySQL 經過判斷不一樣執行計劃的代價來決定的,
            若是MySQL 認爲全表掃效率更高,好比對一些很小的表,它就不會使用索引,這種狀況下 InnoDB 將使用表鎖,而不是行鎖。這種狀況是致命的。

    (2)咱們要使用排他鎖來進行分佈式鎖的 lock,那麼一個排他鎖長時間不提交,就會佔用數據庫鏈接。
      一旦相似的鏈接變得多了,就可能把數據庫鏈接池撐爆


  2.基於redis實現(單機版):須要本身實現 必定要用 SET key value NX PX milliseconds 命令,而不要使用setnx 加expire


    優勢: 性能高、超時失效比數據庫簡單。
    開源實現:Redis官方提出一種算法,叫Redlock,認爲這種實現比普通的單實例實現更安全。
                      RedLock有多種語言的實現包,其中Java版本:Redisson。
    缺點:
    (1)失效時間沒法把控。可能設置太短或者過長的狀況.若是設置太短,其餘線程可能會獲取到鎖,沒法保證狀況。過長時其餘線程獲取不到鎖。
         解決:Redisson的思路:客戶端起一個後臺線程,快到期時自動續期,若是宕機了,後臺線程也沒有了。

    (2)若是採用 Master-Slave 模式,若是 Master 節點故障了,發生主從切換,主從切換的一瞬間,可能出現鎖丟失的問題。
         解決:Redisson ,但存在爭議的,不過應該問題不大。

 

  3.基於zookeeper實現(推薦):可靠性好,使用最普遍。實現:Curator

  4.基於etcd的實現:優於zookeeper實現,若是項目中應用了etcd,那麼使用etcd。

    5.Spring Integration 實現了分佈式鎖:
    Gemfire
    JDBC
    Redis
    Zookeeper

 

基於數據庫實現排他鎖


方案1


獲取鎖

INSERT INTO method_lock (method_name, desc) VALUES ('methodName', 'methodName');

對method_name作了惟一性約束,這裏若是有多個請求同時提交到數據庫的話,數據庫會保證只有一個操做能夠成功。

方案2

 1 DROP TABLE IF EXISTS `method_lock`;  2 CREATE TABLE `method_lock` (  3   `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵',  4   `method_name` varchar(64) NOT NULL COMMENT '鎖定的方法名',  5   `state` tinyint NOT NULL COMMENT '1:未分配;2:已分配',  6   `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,  7   `version` int NOT NULL COMMENT '版本號',  8  `PRIMARY KEY (`id`),  9  UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE 10 ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='鎖定中的方法';

 

先獲取鎖的信息
     select id, method_name, state,version from method_lock where state=1 and method_name='methodName';

佔有鎖
       update t_resoure set state=2, version=2, update_time=now() where method_name='methodName' and state=1 and version=2;

若是沒有更新影響到一行數據,則說明這個資源已經被別人佔位了。

缺點:

    一、這把鎖強依賴數據庫的可用性,數據庫是一個單點,一旦數據庫掛掉,會致使業務系統不可用。
    二、這把鎖沒有失效時間,一旦解鎖操做失敗,就會致使鎖記錄一直在數據庫中,其餘線程沒法再得到到鎖。
    三、這把鎖只能是非阻塞的,由於數據的insert操做,一旦插入失敗就會直接報錯。沒有得到鎖的線程並不會進入排隊隊列,要想再次得到鎖就要再次觸發得到鎖操做。
    四、這把鎖是非重入的,同一個線程在沒有釋放鎖以前沒法再次得到該鎖。由於數據中數據已經存在了。

解決方案:
     一、數據庫是單點?搞兩個數據庫,數據以前雙向同步。一旦掛掉快速切換到備庫上。
     二、沒有失效時間?只要作一個定時任務,每隔必定時間把數據庫中的超時數據清理一遍。
     三、非阻塞的?搞一個while循環,直到insert成功再返回成功。
     四、非重入的?在數據庫表中加個字段,記錄當前得到鎖的機器的主機信息和線程信息,那麼下次再獲取鎖的時候先查詢數據庫,若是當前機器的主機信息和線程信息在數據庫能夠查到的話,直接把鎖分配給他就能夠了。

基於redis實現

   獲取鎖:
        SET resource_name my_random_value NX PX 30000

      解鎖方式一:不可用
    if( (GET user_id) == "XXX" ){ //獲取到本身鎖後,進行取值判斷且判斷爲真。此時,這把鎖剛好失效。
    DEL user_id
    }
    因爲GET取值判斷和DEL刪除並不是原子操做,當程序判經過該鎖的值判斷髮現這把鎖是本身加上的,準備DEL。
    此時該鎖剛好失效,而另一個請求剛好得到key值爲user_id的鎖。
    此時程序執行了了DEL user_id,刪除了別人加的鎖,尷尬!

  解鎖方式二(推薦):爲了保證查詢和刪除的原子性操做,須要引入lua腳本支持。

    

    if redis.call('get',KEYS[1]) == ARGV[1] then 
           return redis.call('del',KEYS[1]) 
    else
      return 0 
    end

 



使用zookeeper實現分佈式鎖

zookeeper分佈式鎖應用了臨時順序節點

獲取鎖

首先,在Zookeeper當中建立一個持久節點ParentLock。當第一個客戶端想要得到鎖時,須要在ParentLock這個節點下面建立一個臨時順序節點 Lock1。

 

以後,Client1查找ParentLock下面全部的臨時順序節點並排序,判斷本身所建立的節點Lock1是否是順序最靠前的一個。若是是第一個節點,則成功得到鎖。

這時候,若是再有一個客戶端 Client2 前來獲取鎖,則在ParentLock下載再建立一個臨時順序節點Lock2。 

Client2查找ParentLock下面全部的臨時順序節點並排序,判斷本身所建立的節點Lock2是否是順序最靠前的一個,結果發現節點Lock2並非最小的。

因而,Client2向排序僅比它靠前的節點Lock1註冊Watcher,用於監聽Lock1節點是否存在。這意味着Client2搶鎖失敗,進入了等待狀態。

這時候,若是又有一個客戶端Client3前來獲取鎖,則在ParentLock下載再建立一個臨時順序節點Lock3。 

 

Client3查找ParentLock下面全部的臨時順序節點並排序,判斷本身所建立的節點Lock3是否是順序最靠前的一個,結果一樣發現節點Lock3並非最小的。

因而,Client3向排序僅比它靠前的節點Lock2註冊Watcher,用於監聽Lock2節點是否存在。這意味着Client3一樣搶鎖失敗,進入了等待狀態。

 

這樣一來,Client1獲得了鎖,Client2監聽了Lock1,Client3監聽了Lock2。這偏偏造成了一個等待隊列,很像是Java當中ReentrantLock所依賴的AQS(AbstractQueuedSynchronizer)。

得到鎖的過程大體就是這樣,那麼Zookeeper如何釋放鎖呢?

釋放鎖的過程很簡單,只須要釋放對應的子節點就好。

釋放鎖

釋放鎖分爲兩種狀況:

1.任務完成,客戶端顯示釋放

當任務完成時,Client1會顯示調用刪除節點Lock1的指令。

 

2.任務執行過程當中,客戶端崩潰

得到鎖的Client1在任務執行過程當中,若是Duang的一聲崩潰,則會斷開與Zookeeper服務端的連接。根據臨時節點的特性,相關聯的節點Lock1會隨之自動刪除。 

 

因爲Client2一直監聽着Lock1的存在狀態,當Lock1節點被刪除,Client2會馬上收到通知。這時候Client2會再次查詢ParentLock下面的全部節點,確認本身建立的節點Lock2是否是目前最小的節點。若是是最小,則Client2瓜熟蒂落得到了鎖。 

 

同理,若是Client2也由於任務完成或者節點崩潰而刪除了節點Lock2,那麼Client3就會接到通知。 

 

最終,Client3成功獲得了鎖。 

相關文章
相關標籤/搜索