redis 实现的一个锁有问题,求大神帮忙看看 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
EchoUtopia
V2EX    程序员

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

  •  
  •   EchoUtopia 2017-07-17 13:18:38 +08:00 6189 次点击
    这是一个创建于 3075 天前的主题,其中的信息可能已经有所发展或是发生改变。

    之前用 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 条附言    2017-07-17 16:42:14 +08:00
    ```
    while True:
    result = re.setnx(_lock_key, "locked")
    if not result:
    time.sleep(0.01)
    else:
    re.expire(_lock_key, self._timeout)
    return
    ```
    这个是我的实现方式,正文里是一个大神的实现方式,都有问题
    31 条回复    2017-07-18 15:31:14 +08:00
    sampeng
        1
    sampeng  
       2017-07-17 15:41:44 +08:00
    多进程操作如果不能保证是原子的。。这种中心锁就没有意义。。。
    tr0uble
        2
    tr0uble  
       2017-07-17 15:46:59 +08:00
    每次 set 的时候设一个随机字符串进去,删的时候要这个字符串匹配才删

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

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

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

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

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


    @luoqeng 有可能是这个原因,但是线上时不时的出问题,应该是有问题的,线上的情景是:新创建用户我们给以下操作加锁:获取最后一个用户id,然后加一个随机数作为新用户id。然后并发的时候两个新用户获取到的 last_id 相同,并且随机数相同了,导致出问题。。
    luoqeng
        8
    luoqeng  
       2017-07-17 16:17:52 +08:00
    「例如某个客户端获得了一个锁,但它的处理时长超过了锁的有效时长,之后它删除了这个锁,而此时这个锁可能又被其他 d 客户端给获得了。仅仅做删除是不够安全的,很可能会把其他客户端的锁给删了。结合上面的代码,每个锁都有个唯一的随机值,因此仅当这个值依旧是客户端所设置的值时,才会去删除它。」 可能就是这个问题吧,引用上面回复的文章: http://zhangtielei.com/posts/blog-redlock-reasoning.html。
    zts1993
        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 都很有争议.
    lolizeppelin
        10
    lolizeppelin  
       2017-07-17 16:36:14 +08:00
    lolizeppelin
        11
    lolizeppelin  
       2017-07-17 16:36:52 +08:00
    我代码都是基于协程的, 不折腾多线程
    lolizeppelin
        12
    lolizeppelin  
       2017-07-17 16:39:25 +08:00
    我的做法是 第一次 set 的时候只有一个很短的 ttl
    成功后在延长这个 key 的生存时间为需要锁定的时间
    EchoUtopia
        13
    EchoUtopia  
    OP
       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
        14
    EchoUtopia  
    OP
       2017-07-17 16:54:24 +08:00
    @zts1993 那个 unlock 按我的理解是,如果 key 被其他人删了,那么会触发它的 watch,然后就不删除key了
    zts1993
        15
    zts1993  
       2017-07-17 16:59:04 +08:00
    @EchoUtopia 太复杂了。
    EchoUtopia
        16
    EchoUtopia  
    OP
       2017-07-17 17:05:29 +08:00
    @lolizeppelin 你这个异步代码写的好6啊、

    @zts1993 什么太复杂了
    mansur
        17
    mansur  
       2017-07-17 17:06:25 +08:00
    只是生成新用户 id 吗?用 mysql 的自增 id,生产了以后插入 redis 队列,取新 id 的时候直接从队列读不就行了。
    lolizeppelin
        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
    zts1993
        19
    zts1993  
       2017-07-17 17:10:27 +08:00
    @EchoUtopia 因为你 watch 前,锁可能被人占了。所以这个 transaction 没有意义。
    fds
        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 什么的我怀疑效果。
    EchoUtopia
        21
    EchoUtopia  
    OP
       2017-07-17 17:31:58 +08:00
    @mansur 最后就是这样改的,但是这个问题还没解决

    @lolizeppelin 你这个是有用到生产环境么,另外你有测试多进程情况吗。那个 setnx 再 expire 应该没问题把,因为 setnx 是原子操作,同时只会有一个实例设置成功,成功后再expire应该也没啥影响吧,没使用一条命令是因为python的redis客户端不支持这样操作


    @zts1993 我的理解是如果锁已经被其他实例占用,那么这个 multi 的命令不会执行,不知道这样理解对不对
    zts1993
        22
    zts1993  
       2017-07-17 17:52:27 +08:00
    @EchoUtopia 问题是开始 watch 得时候 已经改变了. watch 保护的是开始 watch 到你操作这个开始执行得这段时间. 这个时间很短得吧
    EchoUtopia
        23
    EchoUtopia  
    OP
       2017-07-17 18:12:21 +08:00
    @zts1993 哦,这个意思啊,懂了。不过现在遇到的这个问题应该不是打印的,我打印的 lock 到 unlock 的时间都没超过1秒
    lolizeppelin
        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
    sagaxu
        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 了
    lcqtdwj
        26
    lcqtdwj  
       2017-07-17 18:30:24 +08:00
    @zts1993 有什么比较严谨的分布式锁可以用吗? zookeeper?
    EchoUtopia
        27
    EchoUtopia  
    OP
       2017-07-17 18:40:34 +08:00
    @sagaxu 我表述有误,不是随机数,是 random.choice(一个已定义的列表)

    @lolizeppelin redis 在本地,没考虑过这个问题。后面实现改成把 last_user_id 放 redis 了。更改去看源码的时候,突然发现 python redis 自己就实现了一个锁,233
    EchoUtopia
        28
    EchoUtopia  
    OP
       2017-07-17 19:02:02 +08:00
    @lolizeppelin 我使用了新版的 redis 模块:re.set(_lock_key, "locked", nx=True, ex=self._timeout),结果还是一样的,回头再试试这个模块自带的锁
    stone1342006
        29
    stone1342006  
       2017-07-17 19:25:03 +08:00
    先 get 在 setnx 这个没法保证原子性啊
    lolizeppelin
        30
    lolizeppelin  
       2017-07-17 19:45:10 +08:00 via Android
    有问题肯定是你释放有问题捏
    EchoUtopia
        31
    EchoUtopia  
    OP
       2017-07-18 15:31:14 +08:00
    @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 的时候判断了下是不是自己的锁,结果还是一样
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     3025 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 25ms UTC 13:30 PVG 21:30 LAX 05:30 JFK 08:30
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86