需求背景
- 服务端用的Python + gunicorn部署维护服务端程序时发现某些任务会多次下发
- 导致原因
- 原有程序单纯flask,所以对于数据库操作用了线程锁,但是加上gunicorn之后就没办法保证一致性了
解决过程
- 1,用redis做分布式锁,用setnx和expire,但是setnx(set if key not exist)和expire不是原子操作,所以改用set或者lua脚本
- 2,释放锁Python watch,太麻烦,改用lua脚本
- 3,为什么要用uuid做唯一键?
- 1.客户端1获取锁成功
- 2.客户端1在某个操作上阻塞了太长时间
- 3.设置的key过期了,锁自动释放了
- 4.客户端2获取到了对应同一个资源的锁
- 5.客户端1从阻塞中恢复过来,因为value值一样,所以执行释放锁操作时就会释放掉客户端2持有的锁,这样就会造成问题
1,通过redis的setnx解决
# -*- coding: utf-8 -*-
import uuid
import math
import time
from redis import WatchError
def acquire_lock_with_timeout(conn, lock_name, acquire_timeout=3, lock_timeout=2):
"""
基于 Redis 实现的分布式锁
:param conn: Redis 连接
:param lock_name: 锁的名称
:param acquire_timeout: 获取锁的超时时间,默认 3 秒
:param lock_timeout: 锁的超时时间,默认 2 秒
:return:
"""
identifier = str(uuid.uuid4())
lockname = f'lock:{lock_name}'
lock_timeout = int(math.ceil(lock_timeout))
end = time.time() + acquire_timeout
while time.time() < end:
# 如果不存在这个锁则加锁并设置过期时间,避免死锁
if conn.setnx(lockname, identifier):
conn.expire(lockname, lock_timeout)
return identifier
# 如果存在锁,且这个锁没有过期时间则为其设置过期时间,避免死锁
elif conn.ttl(lockname) == -1:
conn.expire(lockname, lock_timeout)
time.sleep(0.001)
return False
def release_lock(conn, lockname, identifier):
"""
释放锁
:param conn: Redis 连接
:param lockname: 锁的名称
:param identifier: 锁的标识
:return:
"""
# python 中 redis 事务是通过pipeline的封装实现的
with conn.pipeline() as pipe:
lockname = 'lock:' + lockname
while True:
try:
# watch 锁, multi 后如果该 key 被其他客户端改变, 事务操作会抛出 WatchError 异常
pipe.watch(lockname)
iden = pipe.get(lockname)
if iden and iden.decode('utf-8') == identifier:
# 事务开始
pipe.multi()
pipe.delete(lockname)
pipe.execute()
return True
pipe.unwatch()
break
except WatchError:
pass
return False
2,通过set命令解决
# -*- coding: utf-8 -*-
import uuid
import math
import time
from redis import WatchError
def acquire_lock_with_timeout(conn, lock_name, acquire_timeout=3, lock_timeout=2):
"""
基于 Redis 实现的分布式锁
:param conn: Redis 连接
:param lock_name: 锁的名称
:param acquire_timeout: 获取锁的超时时间,默认 3 秒
:param lock_timeout: 锁的超时时间,默认 2 秒
:return:
"""
identifier = str(uuid.uuid4())
lockname = f'lock:{lock_name}'
lock_timeout = int(math.ceil(lock_timeout))
end = time.time() + acquire_timeout
while time.time() < end:
# 如果不存在这个锁则加锁并设置过期时间,避免死锁
if conn.set(lockname, identifier, ex=lock_timeout, nx=True):
return identifier
time.sleep(0.001)
return False
def release_lock(conn, lockname, identifier):
"""
释放锁
:param conn: Redis 连接
:param lockname: 锁的名称
:param identifier: 锁的标识
:return:
"""
# python中redis事务是通过pipeline的封装实现的
with conn.pipeline() as pipe:
lockname = 'lock:' + lockname
while True:
try:
# watch 锁, multi 后如果该 key 被其他客户端改变, 事务操作会抛出 WatchError 异常
pipe.watch(lockname)
iden = pipe.get(lockname)
if iden and iden.decode('utf-8') == identifier:
# 事务开始
pipe.multi()
pipe.delete(lockname)
pipe.execute()
return True
pipe.unwatch()
break
except WatchError:
pass
return False
3,通过lua脚本
# -*- coding: utf-8 -*-
import uuid
import math
import time
def acquire_lock_with_timeout(conn, lock_name, acquire_timeout=3, lock_timeout=2):
"""
基于 Redis 实现的分布式锁
:param conn: Redis 连接
:param lock_name: 锁的名称
:param acquire_timeout: 获取锁的超时时间,默认 3 秒
:param lock_timeout: 锁的超时时间,默认 2 秒
:return:
"""
identifier = str(uuid.uuid4())
lockname = f'lock:{lock_name}'
lock_timeout = int(math.ceil(lock_timeout))
end = time.time() + acquire_timeout
while time.time() < end:
# 如果不存在这个锁则加锁并设置过期时间,避免死锁
if conn.set(lockname, identifier, ex=lock_timeout, nx=True):
return identifier
time.sleep(0.001)
return False
def release_lock(conn, lock_name, identifier):
"""
释放锁
:param conn: Redis 连接
:param lockname: 锁的名称
:param identifier: 锁的标识
:return:
"""
unlock_script = """
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
"""
lockname = f'lock:{lock_name}'
unlock = conn.register_script(unlock_script)
result = unlock(keys=[lockname], args=[identifier])
if result:
return True
else:
return False
参考链接
- https://redis.io/topics/distlock/#the-redlock-algorithm
- 知乎:https://zhuanlan.zhihu.com/p/112016634
- 掘金:https://juejin.im/post/5cc165816fb9a03202221dd5