分布式鎖實現


分布式之分布式鎖

1. 分布式鎖

為了解決集群中多主機上不同線程之間的同步,需要在分布式系統中有類似於單主機下用於進程/線程同步的鎖,也即分布式鎖

1.1 基於MySQL

1.1.1 關鍵點

通過使用innodb提供的行鎖來保證互斥性,來作為不同主機上線程的同步

1.1.2 可重入悲觀鎖實現

1)建表
create table if not exists lock_table(
    `id` int primary key ,
    `resource_name` varchar(10) ,
    `locker` varchar(20),
    `reentrant_cnt` int,
    `create_time` bigint(20),
    `update_time` bigint(20),
    unique key(resource_name))
    ENGINE=InnoDB DEFAULT CHARSET=utf8;

其中resource_name也即資源的名稱,locker代表上鎖者,reentrant_cnt代表重入次數

2)上鎖

假設client_1對資源a上鎖,流程如下

select * from lock_table where resource_name = 'a' for update;

這里先檢查是否存在a記錄,如果

  • 存在

    比較記錄中的locker是否是client_1的ip,如果

    • 不是

      等待一段時間后,重新嘗試讀取

    • update lock_table set reentrant_cnt = reentrant_cnt+1 where resource_name = 'a';

  • 不存在

    insert into lock_table(resource_name,locker,reentrant_cnt) value('a','ip:port',1);

3)解鎖

假設client_1對資源a解鎖,流程如下

select * from lock_table where resource_name = 'a' for update;

這里先檢查是否存在a記錄,這里查到是自己的記錄,如果cnt的值

  • 為1

    delete from lock_table where resource_name = 'a'

  • 大於1

    update lock_table set reentrant_cnt = reentrant_cnt-1 where resource_name = 'a';

1.1.3 優劣

    • 容易實現
    • 只使用mysql就可以完成
    • 鎖超時

      需要手動管理鎖的超時

    • 性能差

      需要開啟事務來處理,並發量受限於MySQL的最大連接數

1.2 基於 zookeeper

1.2.1 關鍵點

使用zookeeper的watch機制來滿足互斥性

1)watch機制定義

類比於觀察者模式,支持如下監聽事件

  • NodeCreated 節點創建:exits()
  • NodeDataChanged 節點數據改變:exits()getData()
  • NodeDeleted 節點刪除:exits()getData()getChildren()
  • NodeChildrenChanged 子節點改變:getChildren()

后面的方法均為操作,可以指定標志位代表是否開啟監聽,對應於命令為加入-w參數get -w /root/...

2)watch機制使用流程
  • 客戶端注冊監聽事件及回調函數到客戶端的watchManager
  • 發起開啟監聽的讀操作,假設調用getData()
  • 服務端zookeeper注冊該watch事件
  • 當對應的節點數據改變、刪除時,zookeeper內的事件被消耗,告知所有監聽此節點的客戶端
  • watchManager觸發對應事件的回調函數

1.2.2 可重入悲觀鎖實現

1)上鎖

假設client_1想要對資源resource_1上鎖,流程如下

  • 創建如下的樹結構

    /root  ----> /locks  ----> resource_1 
    	  |             |                
    	  |             |                 
    	   ----> ...     ----> resource_2
    	  |
    	  |
    	 ...
    
  • client_1在resource_1下創建節點,假設為

    • 加讀鎖

      則創建為ip1_port1_readlock_reentrantcnt_lockcnt,reentrantcnt代表重入次數,lock_cnt代表resource_1被上的讀鎖次數,最后的序號為zookeeper給出的,之后,client_1調用getChildren查出resource_1的所有子節點,如果

      • 之前沒有寫鎖,只有讀鎖且序號(lock_cnt)小於自己或沒有讀鎖,則上鎖成功
      • 否則,上鎖失敗,對上一個寫請求的節點注冊watch
    • 加寫鎖,則創建為ip1_port1_writelock_reentrantcnt_lockcnt,reentrantcnt代表重入次數,lock_cnt代表resource_1被上的讀鎖次數,最后的序號為zookeeper給出的,之后,client_1調用getChildren查出resource_1的所有子節點,如果

      • 之前沒有任何鎖或均為自己的上鎖記錄,則上鎖成功
      • 否則,上鎖失敗,對上一個節點注冊watch

當注冊的監聽事件觸發時,就代表上鎖成功

上過鎖的示例圖如下

/root  ----> /locks  ----> resource_1 ----> /ip1_port1_readlock_reentrantcnt_lockcnt-000000
	  |             |                |
	  |             |                 ----> /ip2_port2_writelock_reentrantcnt_lockcnt-000001
	   ----> ...     ----> resource_2
	  |
	  |
	 ...
2)解鎖

只需要刪除對應路徑下的節點即可

1.1.3 優劣

    • 容易實現
    • 創建節點時均為臨時節點,當會話超時節點會被刪除
    • 由於cp特性,可以保證集群內強一致性
    • 集群壓力

      集群應對不了過大的客戶端並發連接數

    • 性能差

      相較於基於緩存的分布式鎖,性能較差

1.3 基於Redis

1.3.1 關鍵點

通過利用redis的緩存特性,主要為存取快及支持過期機制,來實現分布式鎖

1.3.2 悲觀鎖實現

1)上鎖

假設client_1想要對資源a上鎖,流程如下

  • 調用setnx a ip:port,如果

    • 失敗

      則a已經被占用

    • 成功則上鎖完成

  • 調用expire a timeout來設定鎖的過期時間

2)解鎖
  • 判斷a是否存在,有可能時間過長已經過期
  • 如果存在,del a

1.3.3 改進

上面方案存在如下問題

  • 原子性

    如果setnx和expire沒有一次執行完,或者expire沒有執行成功,此時client掛掉,則不會執行del,這樣就產生了死鎖

  • 可重入

    上面方案只可以標記是否占用,不可以標記重入次數

  • 阻塞

    如果set失敗,則需要客戶端周期性嘗試

  • 過期時間過短

    假設client_1對a上鎖后,執行了很長時間,超過了a的過期時間,且client_2在過期時間后,就可以成功對a上鎖,之后client_1執行結束,就會釋放掉不屬於自己的鎖

因而提出下面改進

  • 原子性

    使用set a value ex 5 nx來保證命令的原子性

  • 可重入

    改為使用hash結構,用value表示重入次數

  • 阻塞

    使用redis的發布訂閱模式,來達到異步通知,不需要循環嘗試

  • 過期時間過短

    還是剛才的例子,可以讓client_1創建守護線程在過期時間達到前,檢查是否還占用鎖,如果占用則延長過期時間

# 參考

再有人問你分布式鎖,這篇文章扔給他 - 掘金 (juejin.cn)

ZooKeeper Watch 機制源碼解析 - Spongecaptain 的個人技術博客

14.0 Zookeeper 分布式鎖實現原理 | 菜鳥教程 (runoob.com)

分布式鎖用Redis還是Zookeeper? - 知乎 (zhihu.com)

分布式鎖的實現之 redis 篇 | 小米信息部技術團隊 (xiaomi-info.github.io)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM