文章來源:blog.csdn.net/raoxiaoya/article/de...
作者: raoxiaoya
文章正文
zookeeper和redis實現分散式鎖的對比
1、redis分散式鎖,其實需要自己不斷去嘗試獲取鎖,比較消耗效能;zk分散式鎖,獲取不到鎖,註冊個監聽器即可,不需要不斷主動嘗試獲取鎖,效能開銷較小
2、如果是redis獲取鎖的那個客戶端bug了或者掛了,那麼只能等待超時時間之後才能釋放鎖;而zk的話,因為建立的是臨時znode,只要客戶端掛了,znode就沒了,此時就自動釋放鎖。
分散式鎖原理
這個主要得益於ZooKeeper為我們保證了資料的強一致性。鎖服務可以分為兩類,一個是保持獨佔,另一個是控制時序。
1、保持獨佔,就是所有試圖來獲取這個鎖的客戶端,最終只有一個可以成功獲得這把鎖。通常的做法是把zk上的一個znode看作是一把鎖,通過create znode的方式來實現。所有客戶端都去建立 /distribute_lock 節點,最終成功建立的那個客戶端也即擁有了這把鎖。
2、控制時序,就是所有檢視來獲取這個鎖的客戶端,最終都是會被安排執行,只是有個全域性時序了。做法和上面基本類似,只是這裡 /distribute_lock 已經預先存在,客戶端在它下面建立臨時順序節點。Zk的父節點(/distribute_lock)維持一份sequence,保證子節點建立的時序性,從而也形成了每個客戶端的全域性時序。
獲取鎖
方法1:建立一個臨時節點
在需要獲取排他鎖時,所有的客戶端都會試圖通過呼叫create -e
介面,在/distribute_lock
節點下建立臨時子節點/distribute_lock/lock
。 ZooKeeper會保證在所有的客戶端中,最絡只有一個客戶端能夠建立成功,那麼就可以認為該客戶端
獲取了鎖。同時,所有沒有獲取到鎖的客戶端就需要對 /distribute_lock/lock
節點上註冊一個Watcher
監聽,以便實時監聽到lock
節點的變更情況。如果節點被使用完刪除了,zookeeper要向所有監聽者傳送通知,這會阻塞其他操作,並且會導致所有客戶端來爭搶鎖,這種情況稱為“羊群效應”,試想一下,如果監聽者眾多的話,會拖累效能。
方法2:建立臨時順序節點create -s -e /distribute_lock/lock- data
1、每個試圖加鎖的客戶端都會建立一個臨時順序節點 /distribute_lock/lock-xxxxx
,並且zk可以保證序號連續且唯一;
2、然後獲取 /distribute_lock/
下的所有子節點,並按從小到大排序list;
3、判斷最小節點是不是自己,如果是,證明你就獲取鎖了,可以去處理業務邏輯了;
4、如果不是,獲取到list
中你的上一個節點名稱(不一定是 -1 的那一個,因為此時它對應的客戶端有可能主動放棄了),對其實施
監聽操作 get /distribute_lock/lock-xxxxx watch
如果get監聽失敗了,說明節點已經別清除了,重複 2,3 直到監聽成功
或者獲取鎖,如果監聽成功,就在這裡阻塞,等待通知;
5、如果通知過來了,重複 2,3,4 的步驟,直到獲取鎖,因為上一個節點被釋放的原因並不一定是它得到鎖-使用完-釋放,有可能
是客戶端斷開連線了;
6、鎖用完後記得主動清除,不然要等到心跳檢測的時候才會清除。
對比可以看出,方法2雖然比方法1麻煩一點,但是更加合理。
程式碼實現:test-zookeeper.php
<?php
/*
* zookeeper 類屬性常量參考
* https://www.php.net/manual/zh/class.zookeeper.php#zookeeper.class.constants.perms
*/
class zkCli {
protected static $zk;
protected static $myNode;
protected static $isNotifyed;
protected static $root;
public static function getZkInstance($conf, $root){
try{
if(isset(self::$zk)){
return self::$zk;
}
$zk = new \Zookeeper($conf['host'] . ':' . $conf['port']);
if(!$zk){
throw new \Exception('connect zookeeper error');
}
self::$zk = $zk;
self::$root = $root;
return $zk;
} catch (\ZookeeperException $e){
die($e->getMessage());
} catch (\Exception $e){
die($e->getMessage());
}
}
// 獲取鎖
public static function tryGetDistributedLock($lockKey, $value){
try{
// 建立根節點
self::createRootPath($value);
// 建立臨時順序節點
self::createSubPath(self::$root . $lockKey, $value);
// 獲取鎖
return self::getLock();
} catch (\ZookeeperException $e){
return false;
} catch (\Exception $e){
return false;
}
}
// 釋放鎖
public static function releaseDistributedLock(){
if(self::$zk->delete(self::$myNode)){
return true;
}else{
return false;
}
}
public static function createRootPath($value){
$aclArray = [
[
'perms' => Zookeeper::PERM_ALL,
'scheme' => 'world',
'id' => 'anyone',
]
];
// 判斷根節點是否存在
if(false == self::$zk->exists(self::$root)){
// 建立根節點
$result = self::$zk->create(self::$root, $value, $aclArray);
if(false == $result){
throw new \Exception('create '.self::$root.' fail');
}
}
return true;
}
public static function createSubPath($path, $value){
// 全部許可權
$aclArray = [
[
'perms' => Zookeeper::PERM_ALL,
'scheme' => 'world',
'id' => 'anyone',
]
];
/**
* flags :
* 0 和 null 永久節點,
* Zookeeper::EPHEMERAL臨時,
* Zookeeper::SEQUENCE順序,
* Zookeeper::EPHEMERAL | Zookeeper::SEQUENCE 臨時順序
*/
self::$myNode = self::$zk->create($path, $value, $aclArray, Zookeeper::EPHEMERAL | Zookeeper::SEQUENCE);
if(false == self::$myNode){
throw new \Exception('create -s -e '.$path.' fail');
}
echo 'my node is ' . self::$myNode.'-----------'.PHP_EOL;
return true;
}
public function getLock(){
// 獲取子節點列表從小到大,顯然不可能為空,至少有一個節點
$res = self::checkMyNodeOrBefore();
if($res === true){
return true;
}else{
self::$isNotifyed = false;// 初始化狀態值
// 考慮監聽失敗的情況:當我正要監聽before之前,它被清除了,監聽失敗返回 false
$result = self::$zk->get($res, [zkCli::class, 'watcher']);
while(!$result){
$res1 = self::checkMyNodeOrBefore();
if($res1 === true){
return true;
}else{
$result = self::$zk->get($res1, [zkCli::class, 'watcher']);
}
}
// 阻塞,等待watcher被執行,watcher執行完回到這裡
while(!self::$isNotifyed){
echo '.';
usleep(500000); // 500ms
}
return true;
}
}
/**
* 通知回撥處理
* @param $type 變化型別 Zookeeper::CREATED_EVENT, Zookeeper::DELETED_EVENT, Zookeeper::CHANGED_EVENT
* @param $state
* @param $key 監聽的path
*/
public static function watcher($type, $state, $key){
echo PHP_EOL.$key.' notifyed ....'.PHP_EOL;
self::$isNotifyed = true;
self::getLock();
}
public static function checkMyNodeOrBefore(){
$list = self::$zk->getChildren(self::$root);
sort($list);
$root = self::$root;
array_walk($list, function(&$val) use ($root){
$val = $root . '/' . $val;
});
if($list[0] == self::$myNode){
echo 'get locak node '.self::$myNode.'....'.PHP_EOL;
return true;
}else{
// 找到上一個節點
$index = array_search(self::$myNode, $list);
$before = $list[$index - 1];
echo 'before node '.$before.'.........'.PHP_EOL;
return $before;
}
}
}
function zkLock($resourceId){
$conf = ['host'=>'127.0.0.1', 'port'=>2181];
$root = '/lockKey_' . $resourceId;
$lockKey = '/lock_';
$value = 'a';
$client = zkCli::getZkInstance($conf, $root);
$re = zkCli::tryGetDistributedLock($lockKey, $value);
if($re){
echo 'get lock success'.PHP_EOL;
}else{
echo 'get lock fail'.PHP_EOL;
return ;
}
try {
doSomething();
} catch(\Exception $e) {
echo $e->getMessage() . PHP_EOL;
} finally {
$re = zkCli::releaseDistributedLock();
if($re){
echo 'release lock success'.PHP_EOL;
}else{
echo 'release lock fail'.PHP_EOL;
}
return ;
}
}
function doSomething(){
$n = rand(1, 20);
switch($n){
case 1:
sleep(15);// 模擬超時
break;
case 2:
throw new \Exception('system throw message...');// 模擬程式中止
break;
case 3:
die('system crashed...');// 模擬程式崩潰
break;
default:
sleep(13);// 正常處理過程
}
}
// 執行
zkLock(0);
分別開啟三個視窗 php test-zookeeper.php
1、等待順序執行完。
2、將第二個ctrl+c 掛掉。
系統的學習PHP,關注公眾號後可在公眾號內獲取
1 Vue2.5核心技術原始碼分析
公眾號裡回覆:19082201
2 設計模式例項剖析與深入解讀
公眾號裡回覆:20190714
3 PHP高階實戰教程全集
公眾號裡回覆:20190625
4 與mysql的零距離接觸
公眾號裡回覆:20190128
5 高效能Linux伺服器搭建實戰
公眾號裡回覆:20190622
6 ThinkPHP5底層原始碼分析
公眾號裡回覆:20190621
7 Thinkphp外掛化開發微信系統
公眾號裡回覆:201907282319
8 Laravel 基礎入門到微信商城實戰開發
公眾號裡回覆:08250045
9 PHP非同步通訊框架Swoole實戰
公眾號裡回覆:08250024
更多視訊教程,請在每天分享的文章教程裡獲取,感謝你的支援!
本作品採用《CC 協議》,轉載必須註明作者和本文連結