PHP实现redis分布式锁

好记性不如烂笔头,记录一下,方便以后自己查阅

一、redis单实例情况下

<?php
class RedisLock {
	
	private $redis;
	private $lockKey; // 锁键名
	private $expire; // 锁失效时间(秒)
	private $uniqueId;

	public function __construct($redis, $lockKey, $expire)
	{
		$this->redis = $redis;
		$this->lockKey = $lockKey;
		$this->expire = $expire;
	}
	
	/**
	 * @param $retryTime int 抢锁失败重试次数
	 * @param $retryDelay int 重试延迟时间(微秒)
	 * @return boolean
	 */
	public function lock($retryTime, $retryDelay)
	{
		$this->uniqueId = uniqid();
		while (true) {
			// set key value [EX seconds] [PX milliseconds] [NX|XX] 需要redis 2.6.12版本以上
			// 如果键名不存在则设置值并指定过期时间,原子操作
			if ($this->redis->set($this->lockKey, $this->uniqueId, ['nx', 'ex' => $this->expire])) {
				return true;
			}
			// 没抢到锁,睡眠一段时间后重试,如果重试了指定次数还没抢到则返回false
			if ($retryTime-- <= 0) {
				break;
			}
			usleep($retryDelay);
		}
		return false;
	}
	
	public function unlock()
	{
		if (!$this->uniqueId) {
			return;
		}
		$script = '
            if redis.call("GET", KEYS[1]) == ARGV[1] then
                return redis.call("DEL", KEYS[1])
            else
                return 0
            end
        ';
		// eval方法需要redis 2.6.0版本以上,原子操作
		return $this->redis->eval($script, [$this->lockKey, $this->uniqueId], 1);
	}
}

/************** 测试代码 ******************/
try {
	$redis = new Redis();
	$result = $redis->connect('127.0.0.1', 6379);
	if ($result) {
		$redis->auth('123456');
		$redis->select(0);
		$lock = new RedisLock($redis, 'lockKey', 10);
		$flag = $lock->lock(3, 10000); // 重试3次,每次间隔10毫秒
		if ($flag) {
			echo "抢到了锁,执行业务逻辑...\n";
			$lock->unlock();
			echo "释放锁...\n";
		} else {
			echo "系统繁忙,请稍候再试!\n";
		}
		$redis->close();
	} else {
		echo 'connect redis failed';
	}
} catch (RedisException $e) {
	echo 'RedisException: ' . $e->getMessage();
}


二、redis多实例情况下(各实例之间完全独立)

class RedLock
{
    private $retryDelay;
    private $retryCount;
    private $clockDriftFactor = 0.01;

    private $quorum;

    private $servers = array();
    private $instances = array();

    function __construct(array $servers, $retryDelay = 200, $retryCount = 3)
    {
        $this->servers = $servers;

        $this->retryDelay = $retryDelay;
        $this->retryCount = $retryCount;

        $this->quorum  = min(count($servers), (count($servers) / 2 + 1));
    }

    public function lock($resource, $ttl)
    {
        $this->initInstances();

        $token = uniqid();
        $retry = $this->retryCount;

        do {
            $n = 0;

            $startTime = microtime(true) * 1000;

            foreach ($this->instances as $instance) {
                if ($this->lockInstance($instance, $resource, $token, $ttl)) {
                    $n++;
                }
            }

            # Add 2 milliseconds to the drift to account for Redis expires
            # precision, which is 1 millisecond, plus 1 millisecond min drift
            # for small TTLs.
            $drift = ($ttl * $this->clockDriftFactor) + 2;

            $validityTime = $ttl - (microtime(true) * 1000 - $startTime) - $drift;

            if ($n >= $this->quorum && $validityTime > 0) {
                return [
                    'validity' => $validityTime,
                    'resource' => $resource,
                    'token'    => $token,
                ];

            } else {
                foreach ($this->instances as $instance) {
                    $this->unlockInstance($instance, $resource, $token);
                }
            }

            // Wait a random delay before to retry
            $delay = mt_rand(floor($this->retryDelay / 2), $this->retryDelay);
            usleep($delay * 1000);

            $retry--;

        } while ($retry > 0);

        return false;
    }

    public function unlock(array $lock)
    {
        $this->initInstances();
        $resource = $lock['resource'];
        $token    = $lock['token'];

        foreach ($this->instances as $instance) {
            $this->unlockInstance($instance, $resource, $token);
        }
    }

    private function initInstances()
    {
        if (empty($this->instances)) {
            foreach ($this->servers as $server) {
                list($host, $port, $timeout) = $server;
                $redis = new \Redis();
                $redis->connect($host, $port, $timeout);

                $this->instances[] = $redis;
            }
        }
    }

    private function lockInstance($instance, $resource, $token, $ttl)
    {
        return $instance->set($resource, $token, ['NX', 'PX' => $ttl]);
    }

    private function unlockInstance($instance, $resource, $token)
    {
        $script = '
            if redis.call("GET", KEYS[1]) == ARGV[1] then
                return redis.call("DEL", KEYS[1])
            else
                return 0
            end
        ';
        return $instance->eval($script, [$resource, $token], 1);
    }
}


/************** 测试代码 ******************/
$servers = [
    ['127.0.0.1', 6379, 0.01],
    ['127.0.0.1', 6389, 0.01],
    ['127.0.0.1', 6399, 0.01],
];
$redLock = new RedLock($servers);
$lock = $redLock->lock('my_resource_name', 1000);
if ($lock) {
	// 抢到了锁
	
}
// 无论抢到锁与否,最后都要调用unlock方法
$redLock->unlock($lock);

RedLock类代码来自redis官方 https://redis.io/topics/distlock

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值