php实现zookeeper分布式锁

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

对比可以看出,方法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 挂掉。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值