为什么分布式锁?
在分布式应用场景中在处理事务时我们常需要用到分布式锁。由于在分布式应用中应用在不同的主机中彼此独立,Jvm提供传统的原始锁无法对另一个主机上的应用干涉,导至每个应用持有一个把独立的锁,每个并发请求会出现同时持有数据的独立备份,没有办法保持原子性。于是为了保持事务的整体原子性和有序性,就有了分布式锁。
使用分布式锁之前:
使用分布式锁之后:
分布式锁主流方案:
1、redis
2、zookeeper
Redis分布式锁的主要实现方式
获取锁命令
SET resource_name my_random_value NX PX 30000
JAVA代码
try{
lock = redisTemplate.opsForValue().setIfAbsent(lockKey, LOCK);
logger.info("cancelCouponCode是否获取到锁:"+lock);
if (lock) {
// TODO
redisTemplate.expire(lockKey,1, TimeUnit.MINUTES); //成功设置过期时间
return res;
}else {
logger.info("cancelCouponCode没有获取到锁,不执行任务!");
}
}finally{
if(lock){
redisTemplate.delete(lockKey);
logger.info("cancelCouponCode任务结束,释放锁!");
}else{
logger.info("cancelCouponCode没有获取到锁,无需释放锁!");
}
}
REDIS作为分布式锁的单点问题
使用的要点:
-
同时设置值和过期时间:
如果不用,先设置了值,再设置过期时间,这个不是原子性操作,有可能在设置过期时间之前宕机,会造成死锁(key永久存在。
设置 -
value要具有唯一性
这个是为了在解锁的时候,需要验证value是和加锁的一致才删除key。
这是避免了一种情况:假设A获取了锁,过期时间30s,此时35s之后,锁已经自动释放了,A去释放锁,但是此时可能B获取了锁。A客户端就不能删除B的锁了。 -
redis可用性问题
由于Redis作为分布式锁,而单机模式下的redis会出现可用性问题,当redis运行出现问题,就无法获取到锁。在高并发的场景下,这点十分致命。在主从模式下,即使有哨兵作主从切换,也会使redis出现一段时间的竞态状况,此时就会有可能出现锁丢失的问题。
解决思路: 通过客户端的超时时间维护 判断 redis 是否失效,来避免锁丢失 。
解决框架redissonredisson所有指令都通过lua脚本执行,redis支持lua脚本原子性执行
redisson设置一个key的默认过期时间为30s,如果某个客户端持有一个锁超过了30s怎么办?
redisson中有一个watchdog的概念,翻译过来就是看门狗,它会在你获取锁之后,每隔10秒帮你把 key的超时时间设为30s
这样的话,就算一直持有锁也不会出现key过期了,其他线程获取到锁的问题了。
redisson的“看门狗”逻辑保证了没有死锁发生。
ZOOKEEPER分布式锁的主要实现式
1 阻塞锁和非阻塞锁
根据业务特点,普通分布式锁有两种需求:阻塞锁和非阻塞锁。
阻塞锁:多个系统同时调用同一个资源,所有请求被排队处理。已经得到分布式锁的系统,进入运行状态完成业务操作;没有得到分布式锁的线程进入阻塞状态等待,当获得相应的信号并获得分布式锁后,进入运行状态完成业务操作。
非阻塞锁:多个系统同时调用同一个资源,当某一个系统最先获取到锁,进入运行状态完成业务操作;其他没有得到分布式锁的系统,就直接返回,不做任何业务逻辑,可以给用户提示进行其他操作。
利用 zookeeper 节点的不可重复性创建互斥节点。
实现流程
查看目标Node是否已经创建,已经创建,那么等待锁。
如果未创建,创建一个瞬时Node,表示已经占有锁。
如果创建失败,那么证明锁已经被其他线程占有了,那么同样等待锁。
当释放锁,或者当前Session超时的时候,节点被删除,唤醒之前等待锁的线程去争抢锁。
为了解决惊群问题:
我们将锁抽象成目录,多个线程在此目录下创建瞬时的顺序节点,因为Zk会为我们保证节点的顺序性,所以可以利用节点的顺序进行锁的判断。
首先创建顺序节点,然后获取当前目录下最小的节点,判断最小节点是不是当前节点,如果是那么获取锁成功,如果不是那么获取锁失败。
获取锁失败的节点获取当前节点上一个顺序节点,对此节点注册监听,当节点删除的时候通知当前节点。
当unlock的时候删除节点之后会通知下一个节点。