zookeeper分散式鎖,你用php是如何實現的呀

php自學中心發表於2021-01-21

文章來源:blog.csdn.net/raoxiaoya/article/de...
作者: raoxiaoya

關注php自學中心,獲取各種視訊教程

文章正文

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


更多視訊教程,請在每天分享的文章教程裡獲取,感謝你的支援!
關注php自學中心,獲取各種視訊教程

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章