redis 实现的一个锁有问题,求大神帮忙看看

2017-07-17 13:18:38 +08:00
 EchoUtopia

之前用 redis 实现了一个锁,但是发现这个锁并不能正常工作,经常两个进程同时获得锁,但是我实在看不出哪一步出现问题了,求大家帮忙看看,或者教教我怎么调试,谢谢了。 代码:

        def lock(self):
            _lock_key = self._key['_lock:_**']
            re = self._re
            while True:
                get_stored = re.get(_lock_key)
                if get_stored:
                    time.sleep(0.01)
                else:
                    if re.setnx(_lock_key, 1):
                        re.expire(_lock_key, 5)
                        return True
        
    	def unlock(self):
                _lock_key = self._key['_lock:_**']
                pipeline = self._re.pipeline
                with pipeline() as p:
                try:
                    p.watch(_lock_key)
                    p.multi()
                    p.delete(_lock_key)
                    p.execute()
                except:
                    sys.stderr.write("not deleted\n")

测试方法:5个进程循环20次不断获取锁,sleep 0.01 秒,释放锁。

def test_mutex(name, thread_num):
    for i in xrange(20):
        mutex = Mutex(name, timeout=5)
        mutex.lock()
        sys.stderr.write("locked\n")
        time.sleep(0.01)
        mutex.unlock()
        sys.stderr.write(thread_num + "---unlocked\n\n")

之前 unlock 是简单的 delete 掉 key,然后怀疑delete时已经超时,就改成上面的实现方式,结果还是不行。能帮忙分析下哪步有问题吗,谢了。

5530 次点击
所在节点    程序员
31 条回复
sampeng
2017-07-17 15:41:44 +08:00
多进程操作如果不能保证是原子的。。这种中心锁就没有意义。。。
tr0uble
2017-07-17 15:46:59 +08:00
每次 set 的时候设一个随机字符串进去,删的时候要这个字符串匹配才删

另外:高版本的 set 可以通过加参数实现 nx 和 过期的功能,你可以看看你这个库支不支持

可能并没有解决你的问题,2333
RubyJack
2017-07-17 15:49:01 +08:00
https://redis.io/topics/distlock redis 本身有方案的
luoqeng
2017-07-17 15:49:20 +08:00
调换一下顺序试试,有可能已经解锁,然后另一个进程显示 locked,而当前进程也还没来得及显示 unlocked。
应该是多线程吧,看函数参数。多进程也不好观察调试。

sys.stderr.write(thread_num + "---unlocked\n\n")
mutex.unlock()
sampeng
2017-07-17 15:49:38 +08:00
awanabe
2017-07-17 15:57:49 +08:00
nx 就行了,加个 ttl
EchoUtopia
2017-07-17 16:00:30 +08:00
@sampeng 多线程也是一样,setnx 官方文档并没有说 setnx 是否是原子操作,但网上很多资料都把它当原子操作使用

@tr0uble 这个我考虑过,是因为获得锁的实例超时后导致把别人的锁给删掉,我这个超时时间设的5秒,获得锁的时间为 0.01 秒,我打印时间也表明没有超时

@RubyJack
@sampeng 这个我还没有去看,我现在只是很难过,我不知道到底哪出问题了,并且我没有一点办法,因为太菜,连调试的思路都没有,我之前假装 strace 了以下,问题又不重现了,估计是竟态条件不满足了。


@luoqeng 有可能是这个原因,但是线上时不时的出问题,应该是有问题的,线上的情景是:新创建用户我们给以下操作加锁:获取最后一个用户id,然后加一个随机数作为新用户id。然后并发的时候两个新用户获取到的 last_id 相同,并且随机数相同了,导致出问题。。
luoqeng
2017-07-17 16:17:52 +08:00
「例如某个客户端获得了一个锁,但它的处理时长超过了锁的有效时长,之后它删除了这个锁,而此时这个锁可能又被其他 d 客户端给获得了。仅仅做删除是不够安全的,很可能会把其他客户端的锁给删了。结合上面的代码,每个锁都有个唯一的随机值,因此仅当这个值依旧是客户端所设置的值时,才会去删除它。」 可能就是这个问题吧,引用上面回复的文章: http://zhangtielei.com/posts/blog-redlock-reasoning.html。
zts1993
2017-07-17 16:33:34 +08:00
setnx 没有问题. 可以说是原子的. redis 不可能在处理中打断去处理其他命令,这点可以看 redis 源码.


lock : re.setnx 返回值是什么,我不是太清楚,没有怎么使用 py client, 但是 lock 前几行代码是没有意义得, 你直接根据 setnx 返回值判断就好了, 可靠的. 还有一个问题, 可能需要加上超时时间(防止程序挂掉)
因此应该使用 setnx + setex 也就是那个带有 4 个参数得 set 命令. 具体可以查 redis command


unlock : 写的莫名其妙而且没有任何用处, transaction 使用也不对.


关于锁的释放 : 如果你要保证 delete 时候一定是释放自己得,应该使用 lua 脚本去判断 value 然后 delete,同时创建得时候需要给 id.


结论,不推荐 redis 在严谨的场景下做分布式锁, 即使是 redlock 都很有争议.
lolizeppelin
2017-07-17 16:36:14 +08:00
lolizeppelin
2017-07-17 16:36:52 +08:00
我代码都是基于协程的, 不折腾多线程
lolizeppelin
2017-07-17 16:39:25 +08:00
我的做法是 第一次 set 的时候只有一个很短的 ttl
成功后在延长这个 key 的生存时间为需要锁定的时间
EchoUtopia
2017-07-17 16:53:03 +08:00
@luoqeng 之前我说了,我测验的时候发现并没有超时,并且我的实现里面有 watch key,如果已经超时,应该是不会去删除 key 的

@zts1993 嗯,这个是别人的 lock,我的 lock 是直接去 setnx 的,都不行。超时时间是加了的,在 setnx 成功后,感觉这一步应该没问题,redis.py 没看到 set nx ex 一条命令的用法,要用 lua 脚本,我待会去试试。unlock 的 transaction 怎么用呢,这个是我为了超时加的,但是我的脚本里没有超时,这也是验证过的。


@lolizeppelin 协程多进程下还是会有同样的问题吧,你这个 ttl 操作有啥特殊原因么
EchoUtopia
2017-07-17 16:54:24 +08:00
@zts1993 那个 unlock 按我的理解是,如果 key 被其他人删了,那么会触发它的 watch,然后就不删除key了
zts1993
2017-07-17 16:59:04 +08:00
@EchoUtopia 太复杂了。
EchoUtopia
2017-07-17 17:05:29 +08:00
@lolizeppelin 你这个异步代码写的好6啊、

@zts1993 什么太复杂了
mansur
2017-07-17 17:06:25 +08:00
只是生成新用户 id 吗?用 mysql 的自增 id,生产了以后插入 redis 队列,取新 id 的时候直接从队列读不就行了。
lolizeppelin
2017-07-17 17:08:47 +08:00
1. setnx key 用很短的 ttl 比如 1.5s value 为相关的 id,
用这个 ttl 是因为我的锁是有层级的,设置多个 key 中途会超时
这特短时间的 ttl 能有效释放已经锁住的上层

2. set 成功后,添加一个定时器,定时器触发时间是外部的锁定时间,到时触发删除 key 并通知超时
3. 延长这个 key 的生存时间为 外部所用锁定时间

锁删除之前,先校验 value
这是我的锁的做法


---
如果只要简单的原子锁,set 直接用
set(key, value, px=int(timeout)+3, nx=True)
来设置时间不就好了

不要先 setnx 再 expire
zts1993
2017-07-17 17:10:27 +08:00
@EchoUtopia 因为你 watch 前,锁可能被人占了。所以这个 transaction 没有意义。
fds
2017-07-17 17:29:37 +08:00
首先你这个需求不用 redis 锁,直接在数据库准备个计数器,increase 一个字段,用返回值作为新 id 即可。

如果要在 redis 里用锁,一般都要用 lua 脚本,比如下面这个是类似 setex_if_equal,传个锁的 key,过期时间,和随机生成个 UUID 传入即可
```
local k = KEYS[1]
local ex = ARGV[1]
local eq = ARGV[2]
local v = ARGV[3] or eq
local c = redis.call("GET", k)
if not c or c == eq then
redis.call("SETEX", k, ex, v)
return 1
end
return 0
```
然后写个类似的删除脚本。
脚本的运行过程中,redis 保证是原子的。你用 watch 什么的我怀疑效果。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/375887

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX