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时已经超时,就改成上面的实现方式,结果还是不行。能帮忙分析下哪步有问题吗,谢了。

5299 次点击
所在节点    程序员
31 条回复
EchoUtopia
2017-07-17 17:31:58 +08:00
@mansur 最后就是这样改的,但是这个问题还没解决

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


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

@lolizeppelin redis 在本地,没考虑过这个问题。后面实现改成把 last_user_id 放 redis 了。更改去看源码的时候,突然发现 python redis 自己就实现了一个锁,233
EchoUtopia
2017-07-17 19:02:02 +08:00
@lolizeppelin 我使用了新版的 redis 模块:re.set(_lock_key, "locked", nx=True, ex=self._timeout),结果还是一样的,回头再试试这个模块自带的锁
stone1342006
2017-07-17 19:25:03 +08:00
先 get 在 setnx 这个没法保证原子性啊
lolizeppelin
2017-07-17 19:45:10 +08:00
有问题肯定是你释放有问题捏
EchoUtopia
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 的时候判断了下是不是自己的锁,结果还是一样

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

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

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

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

© 2021 V2EX