Redis、Zookeeper、DB三種分佈式鎖的實現方式

在傳統單機業務系統中,咱們通常經過線程同步方法或同步代碼塊(Java)解決多線程併發場景資源競爭的問題,但當系統擴展到集羣模式的分佈式系統上時,須要實現不一樣主機上多個進程間資源的競爭資源的協調,這時,單進程的鎖失效,鎖也要能支持分佈式。數據庫

分佈式鎖的特性

首先,類比單進程的鎖,咱們來看下分佈式鎖要支持那些特性,纔是一個可用的解決方案。
基本特性(能用)緩存

  1. 互斥性:須要保證鎖只能被分佈式系統中的某臺服務器的某個線程獲取。
  2. 不死鎖:若是得到鎖的線程發生崩潰而沒有釋放鎖,須要保證鎖能釋放被其餘線程獲取。

高級特性(好用)服務器

  1. 可重入:獲取鎖的線程可屢次得到鎖,避免死鎖。
  2. 阻塞鎖:線程阻塞的鎖,簡化客戶端的實現。
  3. 高可用:提供得到鎖和釋放鎖的HA。
  4. 鎖性能:高效得到和釋放鎖。

分佈式鎖的實現方式

  1. 數據庫:藉助數據庫實現分佈式鎖
  2. Redis:基於Redis的分佈式鎖
  3. Zookeeper:基於Zookeeper的分佈式鎖

藉助數據庫實現分佈式鎖

在分佈式系統開始大規模應用前,因爲數據庫都是集中部署的,一些雙機部署的系統要實現分佈式鎖,通常會想到藉助數據庫實現分佈式鎖。
好比在雙機部署系統中,咱們要天天定時給客戶發值班通知短信,因爲兩臺服務器時間一致,會同時運行定時任務,但短信總不能發兩條。多線程

針對這種狀況,能夠在數據庫中設計一張表,記錄某個日期短信是否發送了,表包括日期和短信發生狀態兩個字段,定時任務啓動後,執行以下流程:併發

  1. 根據日期字段查詢數據庫,判斷短信是否發送過了,若是沒查詢到記錄,則插入一條數據,狀態爲todo。
  2. 嘗試經過select for update語句,利用數據自帶排他鎖,鎖定記錄。若是執行成功則表示得到了鎖,繼續執行下一步;若是鎖定不成功,則程序阻塞。
  3. 鎖定成功後,先判斷狀態是否爲todo,若是是,則執行短信發送任務;若是不是,說明短信已發送,rollback釋放鎖,直接返回。
  4. 更新記錄狀態爲done,並經過commit釋放鎖。

以上是一個特殊的場景,從特殊推導出藉助數據庫實現鎖的通常設計。分佈式

  1. 數據庫鎖表(lock_table)結構性能

    1. ID:主鍵,自動生成
    2. lock_name:鎖名稱,鎖的惟一標識
    3. lock_client:鎖的客戶端標識,用於重入時使用
    4. timestamp:鎖最後一次更新時間(暫時無用)
  2. 初始化鎖:向lock_table插入鎖數據,若是已插入,則直接返回。
  3. 獲取鎖:利用數據庫排他鎖,經過select * from lock_table where lock_name=xxx for update獲取鎖。
  4. 更新鎖:獲取到鎖後,更新lock_client爲自身標識。
  5. 執行互斥業務:執行鎖對應的業務邏輯。
  6. 釋放鎖:使用commit提交事務,釋放鎖。

以上步驟,實現了以下特性:線程

  • 若是第三步獲取鎖失敗,則系統會阻塞等待,實現了阻塞鎖
  • select for update實現了互斥鎖
  • lock_client字段實現可鎖可重入
  • 若是客戶端端口,事務自動rollback實現了不會死鎖

若是要實現高可用,則須要數據庫自身支持HA;因爲數據庫鎖開銷比較大,鎖的性能至關較差。設計

基於Redis的分佈式鎖

Redis分佈式鎖,主要經過Redis的setnx(SET IF NOT EXIST)結合緩存過時時間等特性來實現。code

以Spring的RedisTemplate爲例,setnx對應發方法爲:

Boolean setIfAbsent(K key, V value, Duration timeout)

若是Redis緩存中不存在此Key,則建立,並返回true;若是已經存在,則無動做,返回false。

相關設計點以下:

  • 爲了解決可重入問題,咱們把這裏的value和數據庫的lock_client作相同設計;
  • 因爲timeout的存在,能夠解決死鎖問題;
  • 須要注意,若是互斥的業務邏輯,獲取鎖後,在timeout內未執行完,會致使鎖被是否,因此獲取鎖以後,須要啓動一個定時器,在業務執行完成前,按期去延長timeout,防止鎖過時;
  • 這是一把非阻塞鎖,若是線程獲取不到鎖,須要自旋;
  • 鎖業務邏輯執行完後,經過刪除Key來釋放鎖;
  • 調用setIfAbsent,務必調用帶timeout的重載方法,實現建立Key和設置timeout的原子操做,否則可能會出現建立Key後,客戶端崩潰而爲設置timeout,致使緩存永不過時。

基於Zookeeper的分佈式鎖

利用Zookeeper特性,有兩種實現鎖的方式。

  1. 建立節點的排他性:利用建立節點的排他性,多個進程競爭建立一個節點時,只有一個進程能成功得到鎖;其餘競爭此鎖的進程,可經過監聽節點的釋放來獲取鎖。
  2. 臨時有序節點:在同一個目錄下,每一個進程建立本身的的節點,序號最小的節點得到鎖,各進程排隊得到鎖。

原理都比較簡單和直觀,下面簡要描述下臨時有序節點方式的實現原理以下:

  1. 客戶端要獲取鎖是,在Zookeeper指定目錄下建立一個瞬時有序節點;
  2. 判斷本身建立的有序節點是否目錄下序號最小的,若是是最小的則得到鎖,執行業務邏輯;
  3. 若是不是最小的,則監聽目錄下節點的刪除事件,每次有節點刪除後,判斷自身是不是序號最小的,從而確認本身是否得到鎖;
  4. 互斥業務邏輯執行完後,刪除節點,釋放鎖。

相關設計點:

  • 因爲臨時節點在客戶端斷開後自動刪除,可解決死鎖問題。
  • 當自身節點的序號不是最小的時候,經過監聽機制,一直等到自身節點序號爲最小,可實現阻塞鎖
  • 在建立節點是,客戶端把自身信息寫入節點,獲取全部,經過節點信息判斷,可實現鎖重入
  • 因爲ZK自己爲集羣部署,可解決單點問題,實現HA。
相關文章
相關標籤/搜索