Bootstrap

php zookeeper实现分布式锁

php实现zookeeper分布式锁
半人猛犸 2019-08-15 19:23:58 438 收藏 3
展开

php实现zookeeper分布式锁

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、锁用完后记得主动清除,不然要等到心跳检测的时候才会清除。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

对比可以看出,方法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);

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225

分别开启三个窗口 php test-zookeeper.php
1、等待顺序执行完。
2、将第二个ctrl+c 挂掉。
————————————————
版权声明:本文为CSDN博主「半人猛犸」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/raoxiaoya/article/details/99651669

;