关爱redis
描述
基于redis的分布式锁,最近看到分布式锁都是一级分布式锁,而且未拿到锁需要休眠几秒钟继续撞redis,心疼redis三秒钟,本方案采用二级分布式锁,一级锁拿不到就等二级锁,这种不需要几毫秒撞击一次redis,对于超高并发能大大减轻redis的负荷,同时分配锁还能实现时序功能。
安装
pip3 install -r requirements.txt
分布式锁设计
- 分布式锁设计两层,第一层是需要竞争的,第二层是需要等待前一个持有者释放,第二层可以友好获取锁
- 一级锁使用key-value的常规竞争模式
- 第二层是为了防止惊群,采用list队列形式,大家都等待list被填充一个值,被填充表示前面一个持有者释放了锁
- 如果两次获取锁都失败了,那么需要再次获取一级锁,这是为了持有者过期,未将持有权向下传递
z
工作步骤
- 首先获取锁需要提供一个key和value,相同的key获取锁是需要竞争的,其次value具有唯一性,这个用于在释放的时候分析持有锁是否超时被其他人重新获取
- 如果某个任务获取一级锁,它可以继续执行任务,未获得一级锁的任务,可以等待二级锁,二级锁不需要频繁的撞击redis,而是通过list的blpop特性阻塞等待获取
- 如果一级和二级都未获取到锁,最后需要一次兜底处理,再次获取一级锁,目的是防止前面的持有者已经过期,但未释放锁
- 互斥任务处理完毕需要释放锁,释放锁首先要对一级锁和二级锁进行从新设定过期时间,防止时间间隙产生两个持有者,然后获取一级锁的值和自己之前设定的是否一致,如果一致说明自己持有权有效,如果不一致说明持有权过期,被其他的任务夺取
- 如果释放的时候持有权有效则向二级锁添加一条自己value记录,这样等待二级锁的任务就可以获取到消息,如果持有权无效则无需做任何处理
- 当某个任务通过二级锁获取到值后,需要立即修改一级锁的值为自己的值,表示自己拥有所有权,那么释放任务时同上
- 在修改一级锁、等待二级锁、释放锁前后需要同步一级锁与二级锁的时间,这样保证一级锁和二级锁生命周期一致,当没有继承者之后会自动删除
引用
- 以上的逻辑都已经封装在locker.py和async_locker.py中,直接引用该文件即可
# 阻塞式
from locker import Locker
locker = Locker(redis_cli, 'key_name', 'UUID000')
if locker.lock():
do_something()
locker.release_lock()
else: print('fail')
# 协程异步
from async_locker import AioLocker
locker = AioLocker(redis_cli, 'key_name', 'UUID000')
if await locker.lock():
await do_something()
await locker.release_lock()
else: print('fail')
- 核心代码
from datetime import datetime, timedelta
import redis
from math import ceil
class Locker:
def __init__(self, cli: redis.Redis, lock_name, lock_value, time_out=2):
self.lock_name, self.lock_value, self.list_key = lock_name, lock_value, f'{lock_name}_list'
self.cli, self.time_out = cli, ceil(time_out)
self.is_locker = False
# 加锁
def lock(self):
if self._lock_key(): return self.is_locker
if self._lock_list(): return self.is_locker
self._lock_key()
return self.is_locker
# 通过key获取一级锁
def _lock_key(self):
if self.cli.set(self.lock_name, self.lock_value, ex=self.time_out, nx=True):
self.is_locker = True
return self.is_locker
# 通过队列获取二级锁
def _lock_list(self):
self._set_expire()
list_top = self.cli.blpop(self.list_key, self.time_out)
if list_top is not None:
self.is_locker = True
self._set_new_value()
return self.is_locker
# 重新设置值和过期时间
def _set_new_value(self):
self.cli.set(self.lock_name, self.lock_value)
self._set_expire()
# 重新修改过期
def _set_expire(self):
at = datetime.now() + timedelta(seconds=self.time_out)
self.cli.expireat(self.lock_name, at)
self.cli.expireat(self.list_key, at)
# 释放锁
def release_lock(self):
self._set_expire()
if self.cli.get(self.lock_name) == self.lock_value:
self.cli.lpush(self.list_key, self.lock_value)
self._set_expire()
仓库地址:https://gitee.com/favouriter/care4redis