简介

多线程情况下访问一些共享资源需要加锁,否则就会导致数据错乱的问题
分布式锁可以通过DB,Redis,Zk等方式实现,本节主要介绍php使用Redis实现分布式锁

set命令

setnx key value 设置一个值,当key已经存在时,返回flase,代表失败
使用setnx实现分布锁有个缺陷,setnx操作无法设置key的ttl,需要配合exprie key ttl 一起使用
好在set命令就集成了nx和ex操作set key name NX PX 10000
$redis = new Redis();$redis->connect('127.0.0.1', 6380);$rs = $redis->set('testnx', 123, ['nx', 'ex' => 10]);var_dump($rs);

Redlock

set命令还有一个问题,当你要提前释放这个锁的时候,使用expire key 0或者使用del key
如果expire或者del命令发送了阻塞,锁自动失效,这时候B获取了锁,expire/del命令到达,导致B获取的锁失效
Redlock在加锁的时候value值要保证唯一性,在释放锁的时候要验证value是否和申请锁时value是否一致
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],];$redLock = new RedLock($servers);while (true) {    $lock = $redLock->lock('test', 10000);    if ($lock) {        print_r($lock);        $redLock->unlock(['resource' => 'test', 'token' => '5d1c123121538']);    } else {        print "Lock not acquired\n";    }}

后续

Redis分布式锁还有没有问题?


解决方法:引入版本号

参考:https://time.geekbang.org/col...