分布式锁
一、什么是分布式锁
分布式锁是控制分布式系统不同系统之间共同访问共享资源的一种实现
如果不同的系统或同一系统的不同主机之间共享了某个资源时,往往通过互斥来防止彼此的干扰。
二、分布式锁设计目的
可以保证在分布式部署的应用集群当中,同一个方法在同一个操作只能被一台机器上的一个线程执行
三、设计要求
一定要是一把可重入锁(避免产生死锁)
具有高可用的获取锁和释放锁的功能
获取锁和释放锁的性能要好
四、分布式锁的实现方案
基于数据库的实现,数据库的悲观锁实现
基于redis的实现
基于Zookeeper实现分布式锁,ZooKeeper是一个为分布式应用提供一致性服务的开源组件。
五、分布式锁实现方案(redis)
获取锁的时候,使用setnx(setnx key val:当且仅当key不存在的时候,set一个key为val的字符串,返回1则表示成功;若key存在,则什么都不做,返回0)加锁,锁的value值为当前服务器内网IP编号拼接任务标识,在释放锁的时候进行判断,并使用expire命令为锁添加一个超时时间,超过该时间自动释放锁。
获取锁的时候调用setnx,如果返回0,则该锁正在被别人使用,返回1则成功获取锁。还需要设置一个超时时间,因为redis单条命令是原子性操作,所以在加锁和设置超时是两条命令的情况下需要将两个操作同步成一个命令。
通过lua脚本的方式可以使setnx和expire在一条命令同时执行
redis从2.6以后支持setnx、setex连用
通过redission框架
释放锁的时候,判断是不是该锁(即value是否为当前服务器内网IP拼接任务标识),若是该锁。则执行delete删除锁
一定要加finally,在finally里解锁
六、redis分布式锁丢失
6.1 背景
redis分布式锁主节点宕机锁出现丢失。原因如下:
这个master节点由于某些原因发生了主从切换,那么就会出现锁丢失的情况:
在Redis的master节点上拿到了锁;
但是这个加锁的key还没有同步到slave节点;
master故障,发生故障转移,slave节点升级为master节点,锁丢失。
6.2 解决方案
增加Redlock算法,算法流程如下:
获取当前时间戳
client尝试按照顺序使用相同的key,value获取所有redis服务的锁,在获取锁的过程中的获取时间比锁过期时间短很多,这是为了不要过长时间等待已经关闭的redis服务。并且试着获取下一个redis实例。
比如:TTL为5s,设置获取锁最多用1s,所以如果一秒内无法获取锁,就放弃获取这个锁,从而尝试获取下个锁
client通过获取所有能获取的锁后的时间减去第一步的时间,这个时间差要小于TTL时间并且至少有3个redis实例成功获取锁,才算真正的获取锁成功
如果成功获取锁,则锁的真正有效时间是 TTL减去第三步的时间差 的时间;比如:TTL 是5s,获取所有锁用了2s,则真正锁有效时间为3s(其实应该再减去时钟漂移);
如果客户端由于某些原因获取锁失败,便会开始解锁所有redis实例;因为可能已经获取了小于3个锁,必须释放,否则影响其他client获取锁
redlock注意点:
先假设client获取所有实例,所有实例包含相同的key和过期时间(TTL) ,但每个实例set命令时间不同导致不能同时过期,第一个set命令之前是T1,最后一个set命令后为T2,则此client有效获取锁的最小时间为TTL-(T2-T1)-时钟漂移;
对于以N/2+ 1(也就是一半以 上)的方式判断获取锁成功,是因为如果小于一半判断为成功的话,有可能出现多个client都成功获取锁的情况, 从而使锁失效
一个client锁定大多数事例耗费的时间大于或接近锁的过期时间,就认为锁无效,并且解锁这个redis实例(不执行业务) ;只要在TTL时间内成功获取一半以上的锁便是有效锁;否则无效
RedLock性能及崩溃恢复的相关解决方法
如果redis没有持久化功能,在clientA获取锁成功后,所有redis重启,clientB能够再次获取到锁,这样违法了锁的排他互斥性;
如果启动AOF永久化存储,事情会好些, 举例:当我们重启redis后,由于redis过期机制是按照unix时间戳走的,所以在重启后,然后会按照规定的时间过期,不影响业务;但是由于AOF同步到磁盘的方式默认是每秒-次,如果在一秒内断电,会导致数据丢失,立即重启会造成锁互斥性失效;但如果同步磁盘方式使用Always(每一个写命令都同步到硬盘)造成性能急剧下降;所以在锁完全有效性和性能方面要有所取舍;
有效解决既保证锁完全有效性及性能高效及即使断电情况的方法是redis同步到磁盘方式保持默认的每秒,在redis无论因为什么原因停掉后要等待TTL时间后再重启(学名:延迟重启) ;缺点是 在TTL时间内服务相当于暂停状态;
总结:
TTL时长 要大于正常业务执行的时间+获取所有redis服务消耗时间+时钟漂移
获取redis所有服务消耗时间要远小于TTL时间,并且获取成功的锁个数要 在总数的一般以上:N/2+1
尝试获取每个redis实例锁时的时间要远小于TTL时间
尝试获取所有锁失败后 重新尝试一定要有一定次数限制
在redis崩溃后(无论一个还是所有),要延迟TTL时间重启redis
在实现多redis节点时要结合单节点分布式锁算法 共同实现
