一、
分布式锁,的原理,都是,找一个各个服务都能访问的中间层,然后,通过获得这个中间层的占有权达到锁的效果。
1.1、redis分布式锁
redis锁有一个工具包 redisson
里面包装好了,redis锁相关的各种操作,包括可重入锁、公平锁、联锁、红锁、 读写锁等等,这个待会再说。
1.1.1 如果我们设计,会怎么做。
redis锁的大概原理分析如下:
while(true){
if(redis.setnx(key)){
// 加锁成功
,,,处理逻辑
// 释放锁
redis.delete(key);
break;
} else{
Thread.sleep(10);
}
}
缺点:这种方式,一旦加锁的服务挂了,setnx成功,redis.delete(key)永远得不到执行,那么这个锁永远存在,其他服务获取不到锁了。
解决方式:setnx加超时机制,但是因为setnx本身没有超时机制,所以需要分步加。
while(true){
if(redis.setnx(key,value)){
// 30秒过期
redis.expire(key, 30秒);
// 加锁成功
,,,处理逻辑
// 释放锁
redis.delete(key);
break;
} else{
Thread.sleep(10);
}
}
可是这样又有一个问题,如果setnx成功,还没来的急expire,服务就挂了,依然会有锁永远无法释放的问题。
那么假设,加上multi,使得setnx和expire一起执行,那么就不会存在setnx执行后,没有执行expire的问题。但是,multi并非是完全事务的,也就是说,setnx失败,expire还是会执行。所以,可能会造成,key一直被expire刷新过期时间。
而在redis 2.6.12版本之后,set,具有了和setnx一样的功能:
set key value NX PX 30000 :nx是setnx的意思,px是过期时间的意思,30000毫秒过期。
这样,就能保证set和expire原子性了。
但是,这样又会有新的问题,因为有了过期时间,如果一个线程加锁后,执行业务逻辑时间太长,锁超过了30秒过期时间,锁已经过期了,并且已经被别的线程加锁了,然后这个旧线程delete了别人的锁。
解决方案:
1、可以在服务端,加一个延迟队列,比如ScheduledThreadPoolExecutor线程池,延迟10秒,expire刷新一下过期时间,这样,一旦服务挂了,这个延迟线程池也得不到执行了。
2、set key value的时候,value弄一个uuid,唯一标识符。然后delete的时候,进行对比,相同,则删除,不相同,则回滚,报错什么的。
但是,问题是,这种机制,get value和对比value,应该保证原子性,否则,get value之后,删除value之前,value过期了,并且,被别人加锁了,那么删除value还是会删除别人的锁。
解决方案:eval执行lua脚本,保证get和对比value的原子性。
redis提供了通过lua脚本自定义命令的功能,比如:
set foo bar
eval 'if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end' 1 foo bar
如此,基本上是能做到分布式的功能。但是,一旦redis挂了,或者如果是主从结构的redis,master节点挂了,锁还没有同步到从节点,然后从节点就哨兵选举成master节点,就会导致有之间锁的线程,错误。
我个人觉得:上面的redis 节点挂了,导致锁的失败影响是很小的。只会影响到一个锁线程,而且,如果value是uuid的话,那个锁线程如果执行完之后,delete节点的时候,发现uuid对不上,还会回滚。如果是延迟expire刷新过期时间的话,确实会造成,两个线程的delete别人的锁。
1.1.2 redis redlock
redis红锁,的大概思想是,在redis cluster环境下,轮流在多个master节点上建立锁,只要当建立的锁的数量大于master节点数量的一半以上,才算建立成功,这样,一旦一个节点挂了,其他线程,也不可能再在一半master节点以上建立锁成功。
建立的锁也是有失效期的,而且,建立锁时有超时机制,在对一个节点建立锁时间太长了,则超时失败,再另一个节点上建锁。
如果建立失败,则依此删除这个锁。
1.1.3 redission的lock
上面说的,redis锁,直接轮询争抢锁的话,会有一个什么问题呢?
如果很多线程并发轮询,而获取锁的线程,迟迟不释放,则会造成cpu负载过高。
redission的lock的实现机制是,eval lua脚本,然后通过redis的pub/sub发布订阅,加上jdk的locksupport.parkNanos()锁线程的方式,解决了轮旋的方式。
总结:redis 锁,很多人都不建议在生产环境使用,比如上面说的单点redis的问题,集群的red lock 也会有时钟漂移等各种问题。
但我个人觉得,使用redission基本上能够很好的搞定分布式锁。
二、zookeeper锁
zookeeper锁
zookeeper的监听回调结点的删除创建机制可以实现分布式锁的功能,大概原理就是加锁,则看是否锁节点存在,存在则说明别人已经获得锁了,则添加一个节点删除监听,然后,等锁释放后,回去回调这个监听,然后重新获得锁。
释放锁,则删除这个节点。
具体代码,这里写的很好:https://www.jianshu.com/p/5d12a01018e1
如果我们使用zookeeper锁,还是用工具类好,比如Curator,可以参考:
https://www.jianshu.com/p/31335efec309
三、zookeeper锁和redis锁对比
redis分布式锁,轮询获取锁,比较消耗性能,zk分布式锁,监听回调机制,性能开销较小
zookeeper不会存在,服务挂了,锁永远存在的线程,zookeeper可以创建临时节点,zookeeper感应到服务挂了,会自己删除锁节点。
三、数据库实现分布式锁
mysql的for update可以实现锁,但是吞吐量太低了,不考虑。