在前面的文章中,已经实现单实例redis分布式锁,但这种实现是基于单个redis服务,若redis服务不可用,显然所有客户端无法加锁,该实现还未到高可用的水平,因此需要进一步提升分布式的锁的逻辑,好在redis官方提供了相应的权威描述并称之为Redlock,具体参考文章:DLM,这个锁的算法实现了多redis实例(各个redis是相互独立的,没有主从、集群模式)的情况,实现了真正高可用分布式锁。
高可用的分布式锁要求:
1)Mutual exclusion,互斥性,任何时刻只能有一个client获取锁
2)Deadlock free,死锁也必须能释放,即使锁定资源的redis服务器崩溃或者分区,仍然能释放锁
3)Fault tolerance;只要多数互相独立的redis节点,这里不是指主从模式两个节点或者集群模式,(一半以上)在使用,client才可以进行加锁,而且加锁成功的redis服务数量超过半数,且锁的有效时长还未过期,才认为加锁成功,否则加锁失败
Redlock算法说明
首先需理解时钟漂移clock drift概念:服务器时钟偏离绝对参考时钟的差值,例如在分布式系统中,有5台服务器,所有服务器时钟在初始情况下都设置成相同的时间(服务器上没有设置ntp同步)例如都为2019-08-01 10:00:00,随着时间的流逝,例如经过1年后,再“观察”这5台服务器的时间,服务器之间的时间对比,将有可能出现一定的快慢差异:
Server1显示一年后的时间:2020-08-01 10:00:01
Server2显示一年后的时间:2020-08-01 10:00:02
Server3显示一年后的时间:2020-08-01 10:00:02
Server4显示一年后的时间:2020-08-01 09:59:58
Server5显示一年后的时间:2020-08-01 10:00:01
那么由这5台服务器组成的分布式系统,在外侧观察,时钟漂移为=2020-08-01 10:00:02减去2020-08-01 09:59:58=4秒,当然这是累计一年的时钟漂移时长,于是可以计算每秒的时间漂移刻度=4/(3600*24*365),该刻度时长极小完全可以忽略不计,这是redis官方提供这个概念,让分布式锁的redis实现看起来更高级。
redlock加锁流程,假设客户端A按顺序分别在5个完全独立的redis实例作为加锁,如图所示:
1)客户端A在redis01加锁操作前,获取当前时间戳T1
2)客户端A使用相同的key和uuid按顺序在5个redis上set key加锁和设定键的过期时长(有效时长),因为set key操作需要一定时间,因此在set过期时长时,需要set大于加锁所消耗的时长,否则客户端A还未在超过半数redis实例加锁成功前,前面redis set的key就已经先失效了,
错误设置:TTL为1s,例如客户端A在redis01加锁耗时为0.1秒、在redis02加锁耗时为0.5秒,但在redis03加锁耗时为1秒,此时redis01、redis02的key已失效,导致客户端A没能在超过半数(3个)的redis实例上加锁成功
正确设置:TTL为5s,例如客户端A在redis01加锁耗时为0.1秒、在redis02加锁耗时为0.5秒,redis03加锁耗时为1秒,此时redis01、redis02、redis03 key还未失效,客户端A成功在超过半数(3个)的redis实例上加锁,但此时客户端A还不能严格意义上成功获得了分布式锁,还需要进行第3步骤的判断
3)客户端A完成在多个redis实例上加锁后,此刻,锁真正有效时间不是一开始设置TTL的10秒,而是由以下得出:
在5个redis上加锁完后所消耗的时长:set_lock_cost=T5-T1=4s
实际锁的最小有效时长:min_validity=TTL-set_lock_cost-时钟漂移耗时
实际锁的最小有效时长=10s-4s-1s=5s,也就是说客户端A虽然在redis服务器设置有效时长为10s,但扣除一系列的加锁操作耗时后,“redis服务端”留给客户端A的实际有效时长为5秒。如果客户端A能在这5秒内完成任务,且按顺序释放锁,那么客户端A完成了一个完整流程的分布式锁条件的任务。
4)如果客户端A超时等原因无法获得超过半数(3)个以上,则必须解锁所有redis实例,否则影响其他进程加锁
RedLock代码实现:
前一篇文章中已经实现的单服务的redis分布式锁,基于该基础上,实现redlock并不复杂,
import time,datetime
import uuid
import random
import redis
import threading
class RedLockException(Exception):
pass
class RedLock(object):
def __init__(self, locker_key, connection_conf_list=None,
retry_times=3,
retry_interval=200,
ttl=5000,
clock_drift=500):
self.locker_key = locker_key
self.retry_times = retry_times
self.retry_interval = retry_interval
self.global_ttl = ttl
self.clock_drift = clock_drift
self.locker_id = None
self.is_get_lock = False
if not connection_conf_list:
connection_conf_list = [{
'host': '192.168.100.5',
'port': 6379,
'db': 0,
'socket_connect_timeout':1
}]
self.all_redis_nodes = [redis.StrictRedis(**each_conf) for each_conf in connection_conf_list]
self.majority_nodes = len(self.all_redis_nodes) // 2 + 1
def _release_single_lock(self, node):
"""
在redis服务端执行原生lua脚本,只能删除加锁者自己的id,而且是原子删除
:return:
"""
lua_script = """
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
"""
try:
lua_func = node.register_script(lua_script)
lua_func(keys=[self.locker_key], args=[self.locker_id])
except(redis.exceptions.ConnectionError, redis.exceptions.TimeoutError):
pass
def _acqu