分散式之分散式鎖
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)