乐趣区

关于php:解锁redis锁的正确姿势

redis 是 php 的好敌人,在 php 写业务过程中,有时候会应用到锁的概念,同时只能有一个人能够操作某个行为。这个时候咱们就要用到锁。锁的形式有好几种,php 不能在内存中用锁,不能应用 zookeeper 加锁,应用数据库做锁又耗费比拟大,这个时候咱们个别会选用 redis 做锁机制。

setnx

锁在 redis 中最简略的数据结构就是 string。最早的时候,上锁的操作个别应用 setnx,这个命令是当:lock 不存在的时候 set 一个 val,或者你还会记得应用 expire 来减少锁的过期,解锁操作就是应用 del 命令,伪代码如下:

1if (Redis::setnx("my:lock", 1)) {2    Redis::expire("my:lock", 10);
3    // ... do something
4
5    Redis::del("my:lock")
6}

这里其实是有问题的,问题就在于 setnx 和 expire 两头如果遇到 crash 等行为,可能这个 lock 就不会被开释了。于是进一步的优化计划可能是在 lock 中存储 timestamp。判断 timestamp 的长短。

set

当初官网倡议间接应用 set 来实现锁。咱们能够应用 set 命令来代替 setnx,就是上面这个样子

1if (Redis::set("my:lock", 1, "nx", "ex", 10)) {
2    ... do something
3
4    Redis::del("my:lock")
5}

下面的代码把 my:lock 设置为 1,当且仅当这个 lock 不存在的时候,设置实现之后设置过期工夫为 10。

获取锁的机制是对了,然而删除锁的机制间接应用 del 是不对的。因为有可能导致误删他人的锁的状况。

比方,这个锁我上了 10s,然而我解决的工夫比 10s 更长,到了 10s,这个锁主动过期了,被他人取走了,并且对它从新上锁了。那么这个时候,我再调用 Redis::del 就是删除他人建设的锁了。

官网对解锁的命令也有倡议,倡议应用 lua 脚本,先进行 get,再进行 del

程序变成:

1$token = rand(1, 100000);
 2
 3function lock() {4    return Redis::set("my:lock", $token, "nx", "ex", 10);
 5}
 6
 7function unlock() {
 8    $script = `
 9if redis.call("get",KEYS[1]) == ARGV[1]
10then
11    return redis.call("del",KEYS[1])
12else
13    return 0
14end    
15    `
16    return Redis::eval($script, "my:lock", $token)
17}
18
19if (lock()) {
20    // do something
21
22    unlock();
23}

这里的 token 是一个随机数,当 lock 的时候,往 redis 的 my:lock 中存的是这个 token,unlock 的时候,先 get 一下 lock 中的 token,如果和我要删除的 token 是统一的,阐明这个锁是之前我 set 的,否则的话,阐明这个锁曾经过期,是他人 set 的,我就不应该对它进行任何操作。

所以:不要再应用 setnx,间接应用 set 进行锁实现。

退出移动版