分布式锁
分布式锁的理念
- 独占性:任何一个时刻有且仅有一个线程持有
- 高可用:若redis集群环境下,不能因为一个节点挂了而出现获取锁和释放锁失败的情况
- 防死锁:杜绝死锁,必须有超时控制机制或者撤销操作,有个兜底的终止跳出方案
- 不乱抢:防止张冠李戴,不能私下unlock别人的锁,只能自己加锁自己释放
- 重入性:同一个节点的同一个线程如果获得锁之后,它也可以再次获取这个锁
分布式锁逐步剖析
下面的考虑都是建立在分布式锁的前提下(对于单机的锁来说的话并不是完全适用的)
单机下加锁
单机如果都没有加锁,在单价下就会导致超卖,保证不了数据的一致性
加锁解锁保证同时出现并保证调用
释放锁的代码必须放入到finally块中,保证任何情况下都会被执行
加锁必须考虑时间问题
防止加完锁后,服务可能会挂掉,导致这个锁无期限的锁下去,所以对redis分布式锁的key必须加上一个过期时间的限制
加锁和设置过期时间必须保证原子性
否则照样会出现上面的问题(要特别注意,在高并发的场景下,任何的有锁操作或者资源争用,一定要时时刻刻的警惕原子性,防止自己犯错)
张冠李戴,删除了别人的锁
第一个进来的线程自己执行的方法时间过长了,redis的key到期自动失效,这时候第二个线程进来开始加锁干活,第二个还没走完,第一个线程执行完毕,直接删掉了第二个线程的加锁key,这样的话就导致了混乱.(判断value是自己的才去删除,否则不删除)
判断锁是自己的和删除锁非原子性
这样的话你判断完的一瞬间去删除锁还是有个时间差,在高并发下很有可能你删除的锁还是不是自己的,还是会误删,所以高并发的锁相关操作一定要考虑原子性,每一步都要谨慎(采用lua脚本来保证原子性,redis对lua脚本有原生的支持,专门用来确保一系列的操作的原子性)
确保redis锁的过期时间永远大于业务执行时间
可以用看门狗 watchDog解决
redis异步复制锁丢失
redis集群是AP属性,在master和slave切换的时候有可能就造成锁的丢失(主节点还没同步给从节点,主节点就挂了,从节点升级为主节点,锁就丢失了) zookeeper集群是CP属性(zookeeper保证了一致性,整个节点都通知到才算成功,但是他牺牲了高可用性来保证的一致性)
用RedLock之Redisson落地实现解决
Redisson解决方案
分布式锁解决方案
public static String key = "redisLock";
public String redisLock() {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
RLock lock = redisson.getLock(key);
lock.lock();
try {
//doSomeThing
return "完成";
} finally {
//避免张冠李戴
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
RedLock
容错率公式
N(部署台数) = 2*(宕机数量)+1
设计理念
- 获取当前时间,以毫秒为单位
- 依次尝试从5个实例,使用相同的key和随机值获取锁.当向Redis请求获取锁时,客户端应该设置一个超时时间,这个超时时间应该小于锁的失效时间,例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间,这样可以防止客户端在试图与一个宕机的Redis节点对话时长时间处于阻塞状态.如果一个实例不可用,客户端应该尽量尝试去另一个Redis实例请求获取锁
- 客户端通过当前时间减去步骤1记录的时间来计算获取锁使用的时间,当且仅当从大多数(N/2+1)的Redis节点都获取到锁,并且获取锁使用的时间小于锁失效时间,锁才算获取成功
- 如果获取到了锁,其真正有效时间等于初始有效时间减去获取锁所用的时间
- 如果由于某些原因未能获取锁(至少无法在至少N/2+1个Redis实例获取锁,或者获取锁的时间超过了有效时间),客户端应该在所有的Redis实例上解锁(即使某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)
该方案为了解决数据不一致的问题,直接舍弃了异步复制只使用master节点,同时由于舍弃了slave,为了保证可用性,引入了N个主节点,官方建议是5,其实一般3个也行,基本够用了
代码案例
public String getLock(){
String value = UUID.randomUUID() + Thread.currentThread().getName();
RLock lock1 = redissonClient1.getLock(key);
RLock lock2 = redissonClient2.getLock(key);
RLock lock3 = redissonClient3.getLock(key);
RedissonRedLock redLock = new RedissonRedLock(lock1,lock2,lock3);
boolean isLockBoolean;
try {
//waitTime,抢锁的等待时间
//leaseTime就是redis的key的过期时间
isLockBoolean = redLock.tryLock(3,300, TimeUnit.SECONDS);
if (isLockBoolean){
//doSomeThing
return "true";
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
redLock.unlock();
}
return "false";
}
缓存续命(watchDog)
实现原理
额外起一个线程,定期检查线程是否还持有锁,如果有则延长过期时间.Redisson里面就实现了这个方案,使用"看门狗"定期检查(每1/3的锁时间检查一次),如果线程还在持有锁,则刷新过期时间
代码跟踪
redisson的lock方法自带了看门狗,直接进行锁续命
lua脚本加锁解释
参数 | 解释 |
---|---|
KEYS[1]代表的是你加锁的那个key | 就是你自己设置的redis加锁的那个锁的key |
ARGV[2]代表的是加锁的客户端ID | |
ARGV[3]就是锁key的默认生存时间 | 默认30秒 |
如何加锁 | 你要加锁的那个key不存在的话,你就进行加锁 hincrby 7bcf6a9f-e7f7-49b0-9727…:117 1 接着会执行 pexpire lockzzyy 30000 |
- 通过exists判断,如果锁不存在,则设置值和过期时间,加锁成功
- 通过hexists判断,如果锁已经存在,并且锁的是当前的线程,则证明是重入锁,加锁成功(会把hash出来的那个整数值加一)
- 如果锁已经存在,但是锁的不是当前线程,则证明有其他的线程持有锁,返回当前锁的过期时间,此时加锁失败