1、什么是分布式锁
分布式锁可以解决分布在不同机器上的不同进程,要同时操作一个共享资源(例如修改数据库的某一行),实现互斥。
2、怎么实现分布式锁
想要实现分布式锁,必须借助一个外部系统,所有进程都去这个系统上申请加锁。而这个外部系统,必须要实现互斥的能力,即两个请求同时进来,只会给一个进程返回成功,另一个返回失败。
这个外部系统,可以是 MySQL,也可以是 Redis、ZooKeeper、Etcd。
3、redis实现分布式锁
3.1.redis如何实现分布式锁的
加锁主要是使用redis中的SETNX命令,这个命令表示SET if Not eXists,即如果 key 不存在,才会设置它的值,否则什么也不做。
127.0.0.1:6379> SETNX lock 1
(integer) 1 // 客户端1,加锁成功
127.0.0.1:6379> SETNX lock 1
(integer) 0 // 客户端2,加锁失败
释放锁直接使用 DEL 命令删除这个 key 即可。
127.0.0.1:6379> DEL lock
以上是基本的实现分布式锁的加锁与释放锁的原理。但这个分布式锁在实现业务逻辑的时候会碰到各种问题,我们基于这些问题进一步的完善这个分布式锁。
问题1:死锁问题
当遇到以下两种情况时:
-
程序处理业务逻辑异常,没及时释放锁;
-
进程挂了,没机会释放锁。
拿着锁的这个客户端就会一直占用这个锁,而其它客户端就永远拿不到这把锁了,造成死锁问题。
对于情况1,我们的处理方式是通过业务代码try ... catch ... fianlly: redis.del(key)来解决。
对于情况2,我们可以在加锁时,给锁设置有效期。
// 一条命令保证原子性执行
127.0.0.1:6379> SET lock 1 EX 10 NX
OK
这里注意,加锁和给锁设置有效期是两个动作,为了满足这两个动作的原子性,我们用一条redis命令来实现。
如果 客户端1 操作共享资源的时间,「超过」了锁的过期时间,锁被「自动释放」;客户端 2 加锁成功,开始操作共享资源;客户端 1 操作共享资源完成,释放锁(但释放的是客户端 2 的锁)。
根据以上情景,我们可以引出两个问题。一个是锁过期时间问题,一个是锁被别人释放问题。
问题2:锁过期时间问题
客户端在拿到锁之后,在操作共享资源时,遇到的场景有可能是很复杂的,例如,程序内部发生异常、网络请求超时等等。因为这些问题不好确定,因此我们很难预估给锁设置的过期时间是多长。如果预估得太短,就会出现上面人家好好的在操作共享资源,结果锁超时释放的问题。而如果预估时间太长,一旦某个持有锁的客户端释放锁失败,那么就会导致所有其它客户端都无法获取锁,从而长时间内无法正常工作。
解决方案:加锁时,先设置一个过期时间,然后我们开启一个守护线程,定时去检测这个锁的失效时间,如果锁快要过期了,操作共享资源还未完成,那么就自动对锁进行续期,重新设置过期时间。
redisson中的Watch Dog 机制就是用来给锁续约的。保证锁是业务执行完释放的,而不是因为超时释放的。
-
leaseTime,即超时释放时间。如果用户没设置,那么默认就会开启看门狗机制。默认设置的超时释放时间为看门狗时间30s。
-
waitTime,在waitTime时间范围内,如果获取锁失败,会等到这个锁的有效期到了不断重试。如果waitTime时间到了,那么就不会重试,直接返回false。
以下是获取锁成功后如何续约的源码:
进入尝试获取锁方法内
进入scheduleExpirationRenewal方法中
进入renewExpiration方法中
这个延时任务执行的内容如下:
unlock方法里的cancel方法:
综上,一个线程首次获得锁后,就会开启每隔10s(leaseTime=30s)的续约任务。直到这个线程unlock时,这个续约任务才会停止。
问题3:锁被别人释放问题
解决方案:客户端在加锁时,给key的value设置为一个只有自己知道的「唯一标识」进去。这个唯一标识可以是uuid,也可以是uuid+线程id。
// 锁的VALUE设置为UUID
127.0.0.1:6379> SET lock $uuid EX 20 NX
OK
在释放锁时,要先判断这把锁是否还归自己持有。
// 锁是自己的,才释放
if redis.get("lock") == $uuid:
redis.del("lock")
因为GET + DEL 是两条命令,且这两个命令还是必须要原子执行才行。因此我们用Lua 脚本来解决。我们可以把这个逻辑,写成 Lua 脚本,让 Redis 来执行。因为 Redis 处理每一个请求是单线程执行的,在执行一个 Lua 脚本时,其它请求必须等待,直到这个 Lua 脚本处理完成,这样一来,GET + DEL 之间就不会插入其它命令了
总结:在实现redis分布式锁时,
1、实现加锁逻辑,需要设置超时时间、将锁的value设置为唯一标识;
2、操作共享资源时,开启守护线程,定期给锁续期;
3、在释放锁时用lua脚本执行先判断锁是不是自己的再释放锁的两个命令。