php 实现Redis分布式锁

释放双眼,带上耳机,听听看~!

简介


多线程多进程情况下访问一些共享资源需要加锁,否则就会导致数据错乱的问题
分布式锁可以通过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


1
2
3
4
5
1$redis = new Redis();
2$redis->connect('127.0.0.1', 6380);
3$rs = $redis->set('testnx', 123, ['nx', 'ex' => 10]);
4var_dump($rs);//返回true代表加锁成功,返回false代表加锁失败
5

 Redlock


set命令还有一个问题,当你要提前释放这个锁的时候,使用expire key 0或者使用del key
如果expire或者del命令发送了阻塞,锁自动失效,这时候B获取了锁,expire/del命令到达,导致B获取的锁失效
Redlock在加锁的时候value值要保证唯一性,在释放锁的时候要验证value是否和申请锁时value是否一致


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
1class RedLock
2{
3    private $retryDelay;
4    private $retryCount;
5    private $clockDriftFactor = 0.01;
6    private $quorum;
7    private $servers = array();
8    private $instances = array();
9    function __construct(array $servers, $retryDelay = 200, $retryCount = 3)
10    {
11        $this->servers = $servers;
12        $this->retryDelay = $retryDelay;
13        $this->retryCount = $retryCount;
14        $this->quorum  = min(count($servers), (count($servers) / 2 + 1));
15    }
16    public function lock($resource, $ttl)
17    {
18        $this->initInstances();
19        $token = uniqid();
20        $retry = $this->retryCount;
21        do {
22            $n = 0;
23            $startTime = microtime(true) * 1000;
24            foreach ($this->instances as $instance) {
25                if ($this->lockInstance($instance, $resource, $token, $ttl)) {
26                    $n++;
27                }
28            }
29            # Add 2 milliseconds to the drift to account for Redis expires
30            # precision, which is 1 millisecond, plus 1 millisecond min drift
31            # for small TTLs.
32            $drift = ($ttl * $this->clockDriftFactor) + 2;
33            $validityTime = $ttl - (microtime(true) * 1000 - $startTime) - $drift;
34            if ($n >= $this->quorum && $validityTime > 0) {
35                return [
36                    'validity' => $validityTime,
37                    'resource' => $resource,
38                    'token'    => $token,
39                ];
40            } else {
41                foreach ($this->instances as $instance) {
42                    $this->unlockInstance($instance, $resource, $token);
43                }
44            }
45            // Wait a random delay before to retry
46            $delay = mt_rand(floor($this->retryDelay / 2), $this->retryDelay);
47            usleep($delay * 1000);
48            $retry--;
49        } while ($retry > 0);
50        return false;
51    }
52    public function unlock(array $lock)
53    {
54        $this->initInstances();
55        $resource = $lock['resource'];
56        $token    = $lock['token'];
57        foreach ($this->instances as $instance) {
58            $this->unlockInstance($instance, $resource, $token);
59        }
60    }
61    private function initInstances()
62    {
63        if (empty($this->instances)) {
64            foreach ($this->servers as $server) {
65                list($host, $port, $timeout) = $server;
66                $redis = new \Redis();
67                $redis->connect($host, $port, $timeout);
68                $this->instances[] = $redis;
69            }
70        }
71    }
72    private function lockInstance($instance, $resource, $token, $ttl)
73    {
74        return $instance->set($resource, $token, ['NX', 'PX' => $ttl]);
75    }
76    private function unlockInstance($instance, $resource, $token)
77    {
78        $script = '
79            if redis.call("GET", KEYS[1]) == ARGV[1] then
80                return redis.call("DEL", KEYS[1])
81            else
82                return 0
83            end
84        ';
85        return $instance->eval($script, [$resource, $token], 1);
86    }
87}
88
89
90
91$servers = [
92    ['127.0.0.1', 6379, 0.01],
93];
94$redLock = new RedLock($servers);
95while (true) {
96    $lock = $redLock->lock('test', 10000);
97    if ($lock) {
98        print_r($lock);
99        $redLock->unlock(['resource' => 'test', 'token' => '5d1c123121538']);
100    } else {
101        print "Lock not acquired\n";
102    }
103}
104

 

后续

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

                           
解决方法:引入版本号

                          

参考:https://time.geekbang.org/column/article/5175

给TA打赏
共{{data.count}}人
人已打赏
安全技术

C++ lambda表达式

2022-1-11 12:36:11

安全漏洞

预警Apache Solr stream.url SSRF与任意文件读取漏洞

2021-2-26 11:36:11

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索