系统对于业务量的承载难度是呈指数级增长,对于业务量小的系统,完成可以利用单线程模型完成对业务逻辑的处理,并不影响用户的体验以及系统性能,但是对于业务量大项目,单线程处理的逻辑显然不适合用,高并发的系统一方面要考虑用户的体验,同时也要兼顾系统的性能。redis作为系统的缓存中间层,因为操作缓存层的效率要远远高于直接操作数据库,所以能够有效的考虑用户的体验。redis分布式锁作为处理高并发业务中的最基础的业务模型是值得了解以及学习的。
redis分布式锁常用业务场景主要集中于高并发场景的秒杀系统或者是接口的幂等性校验。此博客结合高并发场景下的秒杀系统利用Python的协议以及上下文管理器实现以及设计模式中的单例模式实现Redis连接注册器,并且结合redis的常用命令实现分布式锁的业务模型。
多个请求同时争抢锁的同时,必然会有一个请求拿到锁,而其他线程处于等待的状态,循环的申请锁判断是否被释放。拿到锁的请求开始执行业务逻辑,完成业务逻辑之后释放锁。当然,一把分布式锁的业务逻辑没有这么简单而且这段业务逻辑中有许多的坑等着去踩。
- 在请求锁的执行业务逻辑的过程中,如果执行业务逻辑出现异常或者服务器突然宕机,这些情况无法避免,这种情况下导致拿到锁的请求业务逻辑无法再次向下执行,那么锁也就无法释放。其他的请求无法申请到锁同时也就无法执行业务逻辑。对于业务逻辑出现异常这种情况,采用捕获异常的方式完全可以避免,但是对于服务器宕机这种情况,该如何处理 ?最佳的答案就是为锁设置时效时间,假设服务器宕机,锁失效之后redis就会自动释放锁让其他请求申请锁继续执行业务逻辑。
- 对于锁失效还有另外一种情况就是,就是执行业务逻辑的时间远远大于锁失效时间导致业务逻辑没有执行完成而放任其他请求拿到锁执行业务逻辑,解决这种情况的方法就是为当前请求设置唯一的标识符,在释放锁的时候检测是否为当前请求申请释放。同时如果其他请求执行业务逻辑的时间远远大于当前请求,导致当前请求的锁被其他请求释放,这种情况下特别用以导致数据出错,,同时,在请求拿到锁之后开启子线程循环检测当前锁时候失效,如果失效为当前锁重新设置失效时间。
- redis注册器的实现
class RedisConnectHandler :
# 创建redis连接模块
# 利用单例模式实现,节省创建对象占有的内存
# 利用协议实现上下文管理器
def __new__(cls, *args, **kwargs):
if not hasattr(cls,'instance') :
cls.instance = super().__new__(cls)
return cls.instance
def __init__(self,localhost,localport) :
self.connect = redis.Redis(host = localhost,port= localport)
def __enter__(self):
return self.connect
def __exit__(self, exc_type, exc_val, exc_tb):
self.connect.close()
- 循环检测逻辑的实现
def sensitization(executor,lapse_times,localName) :
"""
:param executor: 传递过来的redis的连接对象
:param lapse_times: 设置的锁失效时间
:param localName: redis锁标识符
"""
# 循环检测主线程的锁是否失效,循环检测时间设置为失效时间的1/3
# 如果主线程未执行完毕,但是锁失效,那么重新设置锁的时效时间
while True :
result = executor.exists(localName)
if result == 1 :
executor.expire(name=localName, time= lapse_times)
time.sleep(lapse_times // 3)
- 主业务逻辑的实现
def runner(redisHost,redisPort,localName,lapse_times) :
"""
:param redisHost: redis连接ip
:param redisPort: redis连接端口
:param localName: redis锁标识符
:param localId: 设置的锁标识Id
:param lapse_times: 设置的锁失效时间
"""
# 多线程争抢锁之后,为争抢到锁的线程利用uuid设置唯一标识
# 争抢到锁的线程作为父线程开启子线程用于循环检测锁是否失效
# 线程逻辑执行完成之后,释放锁,将权限交给其他线程
localId = str(uuid.uuid1())
with RedisConnectHandler(localhost= redisHost, localport= redisPort) as executor :
result = executor.setex(name= localName, time= lapse_times, value=localId)
sender = threading.Thread(target= sensitization, args=(executor,lapse_times,localName))
sender.setDaemon(daemonic=True)
sender.start()
if result:
try:
#注入业务逻辑的代码
pass
finally:
if executor.get(name=localName) == localId:
executor.delete(localName)