redis实现分布式锁的原理
一、为什么使用分布式锁?
·>本地锁的局限性:
本地锁只能锁住当前服务,只能保证自己的服务,只有一个线程可以访问,但是在服务众多的分布式环境下,其实是有多个线程同时访问的同一个数据,这显然是不符合要求的。
·>分布式锁的概念:
分布式锁指的是,所有服务中的所有线程都去获得同一把锁,但只有一个线程可以成功的获得锁,其他没有获得锁的线程必须全部等待,等到获得锁的线程释放掉锁之后获得了锁才能进行操作。Redis官网中,set key value有个带有NX参数的命令,这是一个原子性加锁的命令,指的是此key没有被lock是,当前线程才能加锁,如果已经被占用,就不能加锁。
二、redis实现分布式锁的原理?
1.抢占分布式锁:
Java代码中的实现:
Boolean lock = redisTemplate.opsForValue().setIfAbsent( "lock","111");
·如果加锁成功(lock = true)**,就先执行相应的业务,
然后释放掉锁:redisTemplate .delete(key: "lock" );
·如果加锁失败(lock = false)**,就通过自旋的方式进行重试(比如递归调用当前方法)。
注意:
为了防止在执行删锁操作之前,程序因为出现异常导致在还没有执行到删锁命令之前,程序就直接抛出异常退出,导致锁没有释放造成最终死锁的问题。(可能会有人想到,把删锁操作放在finally里以保证删锁操作一定被执行到,但是万一在执行删锁操作的过程中,电脑死机了呢!结果锁还是没有被成功的释放掉,依然会出现死锁现象。)于是,初步想到的解决方式就是在加锁的时候,就给这个锁设置一个过期时间。这样的话,即使我们由于各种原因没有成功的释放锁,redis也会根据过期时间,自动的帮助我们释放掉锁。
2.加锁的同时设置过期时间:
在成功获取到锁之后,执行删锁操作之前,给锁lock设置一个过期时间,例如30秒。
redisTemplate.expire( "lock" , 30, TimeUnit.SECONDS);
这样一来,即使我们自己没有删除掉锁,到到了过期时间后,redis也会帮我们自动删除掉。
注意:
由于加锁和设置锁的过期时间这两步操作不是原子性的,所以可能会在这之间出现问题,导致还没来得及设置锁的过期时间,程序就中断了。所以,需要加锁和设置过期时间这两步必须是原子性不可分割的操作。
Redis中的原子性命令,set lock 111 EX 30 NX ,表示key为lock,值为111,有效时间是30秒,是个NX的原子性加锁操作,可以保证加锁和过期时间这两个操作要么同时成功,要么同时失败。
Java中的代码是:
Boolean lock = redisTemp1ate.opsForValue().setIfAbsent("lock" , "111",30,TimeUnit.SECONDS);
3.使用redis脚本解锁:
上面,我们通过在加锁的同时就给锁设置过期时间的方式,解决了自己因为各种原因无法成功删除掉锁的问题。但是,即使我们利用redis自动删除过期键的方式成功防止了出现死锁的问题,但是在删锁这里,依旧还会出现问题。
例如:
线程一在加锁的时候设置了30秒的过期时间,他成功的获得了锁,然后开始执行自己的业务逻辑部分,如果他的操作过于费事,需要执行40秒种,那么,当他的业务逻辑执行到30秒的时候,由于最开始设置了过期时间是30秒,此时,redis根据过期时间,将线程一占到的锁释放掉,其他线程立马前来占锁,如果线程二此时占锁成功,也开始执行自己的业务逻辑部分,刚执行了10秒,此时线程一执行完了所有的业务逻辑,准备删锁离开,那么线程一此时删锁操作删掉的其实是线程二的锁。但是此时此刻,线程二是需要用这个锁的,却意外的被线程一给释放掉了,这显然是不合理的。所以我们应该在删锁之前先判断一下,当前的锁是否是自己当时加的那把锁,以防止删错删成了别人的锁。
Java代码根据key得到value:
String lockValue = redisTemplate.opsForvalue( ).get("lock" );
将当前的值与最开始存进去的值进行比较,如果相同在执行删锁操作。
但这又会出现问题,试想,如果当前锁的有效时间是10秒,我们的逻辑代码执行了9秒,然后去远程redis中,取当前key对应的value。例如,从本地发送请求到redis查数据的过程花费了0.5秒,redis接收的命令并将对应的值查到共花费了0.3秒,到这里一共是9.8秒,然后,redis将查到的值传回来需要0.6秒,此时已经花费了10.4秒。此时此刻,我们从redis中得到的返回值确实是我们之前存进去的那个值,但实际上,在数据从redis传回来的半路上,我们的值就已经在redis中过期了,此时此刻,redis中存的数据,其实是其他线程存进去的,并不是我们当初存进去的那个数据了,我们此时执行的删锁操作,其实删除的还是其他人的锁。
到这里我们可以知道,删锁出现问题的原因同上面加锁时是一样的,加锁问题解决的方式是:让加锁和设置过期时间的操作是个原子不可分割的过程。同理,那删锁时,我们如果能够保证去redis获取值和删锁操作也是个原子不可分割的过程,就可以解决上述问题了。
为此,redis官网http://redis.cn/commands/set.html提供了一个用于解决此问题的解锁脚本。
Java代码通过解锁脚本进行删锁操作:
String script =
"if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
//使用函数执行删锁脚本
stringRedisTemplate.execute(new DefaultRedisScript<>(script,Integer.class),Arrays.asList("lock"),uuid);
三、Java代码实现redis分布式锁
综上,使用redis实现分布式锁的java代码如下: