之前用 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时已经超时,就改成上面的实现方式,结果还是不行。能帮忙分析下哪步有问题吗,谢了。
1
sampeng 2017-07-17 15:41:44 +08:00
多进程操作如果不能保证是原子的。。这种中心锁就没有意义。。。
|
2
tr0uble 2017-07-17 15:46:59 +08:00
每次 set 的时候设一个随机字符串进去,删的时候要这个字符串匹配才删
另外:高版本的 set 可以通过加参数实现 nx 和 过期的功能,你可以看看你这个库支不支持 可能并没有解决你的问题,2333 |
3
RubyJack 2017-07-17 15:49:01 +08:00
https://redis.io/topics/distlock redis 本身有方案的
|
4
luoqeng 2017-07-17 15:49:20 +08:00
调换一下顺序试试,有可能已经解锁,然后另一个进程显示 locked,而当前进程也还没来得及显示 unlocked。
应该是多线程吧,看函数参数。多进程也不好观察调试。 sys.stderr.write(thread_num + "---unlocked\n\n") mutex.unlock() |
5
sampeng 2017-07-17 15:49:38 +08:00 1
|
6
awanabe 2017-07-17 15:57:49 +08:00
nx 就行了,加个 ttl
|
7
EchoUtopia OP @sampeng 多线程也是一样,setnx 官方文档并没有说 setnx 是否是原子操作,但网上很多资料都把它当原子操作使用
@tr0uble 这个我考虑过,是因为获得锁的实例超时后导致把别人的锁给删掉,我这个超时时间设的5秒,获得锁的时间为 0.01 秒,我打印时间也表明没有超时 @RubyJack @sampeng 这个我还没有去看,我现在只是很难过,我不知道到底哪出问题了,并且我没有一点办法,因为太菜,连调试的思路都没有,我之前假装 strace 了以下,问题又不重现了,估计是竟态条件不满足了。 @luoqeng 有可能是这个原因,但是线上时不时的出问题,应该是有问题的,线上的情景是:新创建用户我们给以下操作加锁:获取最后一个用户id,然后加一个随机数作为新用户id。然后并发的时候两个新用户获取到的 last_id 相同,并且随机数相同了,导致出问题。。 |
8
luoqeng 2017-07-17 16:17:52 +08:00
「例如某个客户端获得了一个锁,但它的处理时长超过了锁的有效时长,之后它删除了这个锁,而此时这个锁可能又被其他 d 客户端给获得了。仅仅做删除是不够安全的,很可能会把其他客户端的锁给删了。结合上面的代码,每个锁都有个唯一的随机值,因此仅当这个值依旧是客户端所设置的值时,才会去删除它。」 可能就是这个问题吧,引用上面回复的文章: http://zhangtielei.com/posts/blog-redlock-reasoning.html。
|
9
zts1993 2017-07-17 16:33:34 +08:00 1
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 都很有争议. |
10
lolizeppelin 2017-07-17 16:36:14 +08:00
|
11
lolizeppelin 2017-07-17 16:36:52 +08:00
我代码都是基于协程的, 不折腾多线程
|
12
lolizeppelin 2017-07-17 16:39:25 +08:00
我的做法是 第一次 set 的时候只有一个很短的 ttl
成功后在延长这个 key 的生存时间为需要锁定的时间 |
13
EchoUtopia OP @luoqeng 之前我说了,我测验的时候发现并没有超时,并且我的实现里面有 watch key,如果已经超时,应该是不会去删除 key 的
@zts1993 嗯,这个是别人的 lock,我的 lock 是直接去 setnx 的,都不行。超时时间是加了的,在 setnx 成功后,感觉这一步应该没问题,redis.py 没看到 set nx ex 一条命令的用法,要用 lua 脚本,我待会去试试。unlock 的 transaction 怎么用呢,这个是我为了超时加的,但是我的脚本里没有超时,这也是验证过的。 @lolizeppelin 协程多进程下还是会有同样的问题吧,你这个 ttl 操作有啥特殊原因么 |
14
EchoUtopia OP @zts1993 那个 unlock 按我的理解是,如果 key 被其他人删了,那么会触发它的 watch,然后就不删除key了
|
15
zts1993 2017-07-17 16:59:04 +08:00
@EchoUtopia 太复杂了。
|
16
EchoUtopia OP |
17
mansur 2017-07-17 17:06:25 +08:00
只是生成新用户 id 吗?用 mysql 的自增 id,生产了以后插入 redis 队列,取新 id 的时候直接从队列读不就行了。
|
18
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 |
19
zts1993 2017-07-17 17:10:27 +08:00
@EchoUtopia 因为你 watch 前,锁可能被人占了。所以这个 transaction 没有意义。
|
20
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 什么的我怀疑效果。 |
21
EchoUtopia OP @mansur 最后就是这样改的,但是这个问题还没解决
@lolizeppelin 你这个是有用到生产环境么,另外你有测试多进程情况吗。那个 setnx 再 expire 应该没问题把,因为 setnx 是原子操作,同时只会有一个实例设置成功,成功后再expire应该也没啥影响吧,没使用一条命令是因为python的redis客户端不支持这样操作 @zts1993 我的理解是如果锁已经被其他实例占用,那么这个 multi 的命令不会执行,不知道这样理解对不对 |
22
zts1993 2017-07-17 17:52:27 +08:00
@EchoUtopia 问题是开始 watch 得时候 已经改变了. watch 保护的是开始 watch 到你操作这个开始执行得这段时间. 这个时间很短得吧
|
23
EchoUtopia OP @zts1993 哦,这个意思啊,懂了。不过现在遇到的这个问题应该不是打印的,我打印的 lock 到 unlock 的时间都没超过1秒
|
24
lolizeppelin 2017-07-17 18:14:51 +08:00
这个只要服务端支持就可以
新版的 python-redis 支持 旧版的 python 的 redis 客户端不支持可以自己封装 python-redis 的源码很简单的,怎么封装自己过一便 话说你们连 python-redis 的源码都没看过? 能一次操作当然要一次做,你先 set 在 expire 分成了两次通信 间隔较大的情况下你 expire 失败了回头删 key 搞不好就不是你设置的 key 了 而且还影响性能 本来你这个需求(用于约束用户 id )就会有不小的性能问题,还分两次问题更加多 顺便,楼上也有人提到了,约束用户 id 不应该用锁来实现 如果只是想唯一 key 的话,比较好的做法是程序那边实现一个类似 Snowflake 的唯一主键生成即可 比用 redis 队列 mysql 字段来弄这性能好多了 我那玩意是写给我的运维管理工具用的,算是写着玩的,不要拿去直接用,有问题不负责 233 |
25
sagaxu 2017-07-17 18:18:56 +08:00 via Android
@EchoUtopia 假设 A 获得的 last_id 是 100,B 获得的 last_id 是 200,A 的随机数是 300,B 的随机数是 200,你就有两个 400 了
|
27
EchoUtopia OP @sagaxu 我表述有误,不是随机数,是 random.choice(一个已定义的列表)
@lolizeppelin redis 在本地,没考虑过这个问题。后面实现改成把 last_user_id 放 redis 了。更改去看源码的时候,突然发现 python redis 自己就实现了一个锁,233 |
28
EchoUtopia OP @lolizeppelin 我使用了新版的 redis 模块:re.set(_lock_key, "locked", nx=True, ex=self._timeout),结果还是一样的,回头再试试这个模块自带的锁
|
29
stone1342006 2017-07-17 19:25:03 +08:00
先 get 在 setnx 这个没法保证原子性啊
|
30
lolizeppelin 2017-07-17 19:45:10 +08:00 via Android
有问题肯定是你释放有问题捏
|
31
EchoUtopia OP @stone1342006
那个应该没影响,我改成 re.set(_lock_key, "locked", nx=True, ex=self._timeout)是一样的 @lolizeppelin ``` def unlock(self): _lock_key = self._key['_lock:_HolytreeTech'] pipeline = self._re.pipeline with pipeline() as p: try: p.watch(_lock_key) lock_ident = p.get(_lock_key) p.multi() if lock_ident != self._ident: return p.delete(_lock_key) p.execute() except: sys.stderr.write("not deleted\n") ``` 我 unlock 的时候判断了下是不是自己的锁,结果还是一样 |