一、分布式锁使用的情形如下,下图中单机锁不能保证资源互斥
一般来说分布式锁使用第三方(外部)系统来保证互斥,常见的有Zookeeper,MySQL,Redis,所有的分布式锁构建都应该注意以下几点要素
1:不能有死锁,进程不能因为出现异常就不释放锁
2:进程在锁上要有唯一标识,只能释放自己加的锁
3:保证对锁的操作是原子性的
4:锁租期
本博客主要介绍使用Redis构建分布式锁,先从简单的开始说明
1:既然是锁,那就需要使用Redis实现互斥的逻辑,使用最简单的String类型K-V格式
127.0.0.1:6379> SETNX dlock 1
(integer) 1 #进程1
127.0.0.1:6379> SETNX dlock 1
(integer) 0 #进程2
加锁成功的进程,就可以去操作被锁住的共享资源了,当操作完成后接着释放锁就可以了。
127.0.0.1:6379> DEL dlock // 释放锁
(integer) 1 #进程1释放锁
既然加锁是SETNX,解锁就可以直接DEL,这种简单的步骤大致就是这样:
但是,这种实现方式是存在问题的:
1:拿到锁的进程1操作共享资源时(例如调用API)阻塞住了,出异常了,不能释放锁
2:进程1被kill掉了
这样的话违背了上面说的,实现分布式锁的注意事项,出现了死锁
2:要解决死锁的问题,只要设置进程对锁的使用时间就好了,简单来说直接给这个key加上过期时间就行了,这样的话就算进程死掉了锁还是能释放的
127.0.0.1:6379> SET dlock 1 EX 10 NX
OK #进程1加锁
虽然这样确实避免了死锁的问题,但是会存在如下新的两个问题
1:进程1加锁成功,开始进行业务操作
2:进程1业务时间,超过了锁的过期时间,锁被自动释放(锁租期不够)
3:进程2加锁成功,开始操作共享资源
4:进程1操作共享资源完成,释放锁(释放了非本进程的锁)
问题1:锁租期不够
这个问题人为的估算肯定是不行的,因为无法预知进程x执行业务到底需要多少时间,估算长了浪费锁资源,短了就锁不住资源,出现了资源不互斥的问题
目前通用的解决方法是:加锁时,先设置一个过期时间,然后我们开启一个守护线程/进程,定时去检测这个锁的失效时间,如果锁快要过期了,业务还未完成,那么就自动对锁进行续期,重新设置过期时间,避免锁过期
Redisson就是使用该方式进行锁租期的续费,类似于下图
这样就可以解决问题了,避免锁提前到期产生的一系列问题
问题2:释放了非本进程的锁
这个问题主要是因为进程在释放锁的时候不知道锁的持有者是谁,这一点其实很好解决,就类似于Monitor Record那种,在加一个进程/线程标识就可以了,只要记住锁是谁持有的就行
127.0.0.1:6379> SET dlock {pid} EX 20 NX
这样在释放锁的时候只需要判断下就可以了,很方便,业务代码这么写
RBucket<Long> bucket = client.getBucket("dLock"); if (Thread.currentThread().getId() == bucket.get()){ // todo 释放锁 }
但是,获取锁的Value和删除锁,在业务里不是原子性操作,就会导致
1:进程1执行GET,判断锁是自己的
2:进程2执行了上面的SET命令,获取到锁(发生概率比较低但是还是有可能的)
3:进程1执行DEL,却释放了客户端 2 的锁(又出现了释放别人的锁的问题)
这个时候就得用Lua脚本来执行获取-删除这个业务了,Redis也可以执行Lua脚本,因为Redis在命令执行的过程中是单线程的,所以不会被其他进程中途插入执行,脚本逻辑类似于这样
if redis.call("GET",KEYS[1]) == ARGV[1]
then
return redis.call("DEL",KEYS[1])
else
return 0
end
这样,释放他人的锁的问题也算是解决了
现在Redis的加锁/解锁的流程已经很流畅了,解决了刚刚提出的问题,总结如下:
1:加锁:SET key {pid} EX {time} NX
2:执行业务访问资源
3:释放锁,这里使用脚本
到现在为止,对分布式锁的讲解还是停留在,非主从模式下的redis,一般来说正式环境都是使用主从模式或者是cluster模式,所以还会有新的问题产生,这个问题流到下期再介绍