redis作为集中式缓存,可以通过它来实现分布式锁。
首先用到的redis操作有:
setnx key value: 当key不存在的时候生效并返回1,当已经有此key的时候返回0
getset key value: 设置新值返回旧值,如果之前不存在也设置新值并返回nil
get key: 返回对应的值,没有则返回nil
del key,key1,key2: 删除,返回删除成功的个数
方案1,使用setnx key value来获得锁,del来释放锁
图1
方案1有个问题是,如果获得锁的线程或者进程崩溃了,这个锁将得不到释放
方案2我们增加将值设置为当前时间,引入超时判断
图2
方案2依然有个问题,并发情况下,同时判断锁超时,n0,n1,n2同时del并通过setnx获取锁可能,会n2把n1获取的锁给删除掉。
方案3增加getset 获取旧的时间设置新的时间,这样删除某个超时锁的操作只有一起,其他的获取锁判断会接下来判断锁没有超时。
图3
注意虚线部分,这是之前其他某个博主的做法,就是谁设置了有效时间戳后就认为获取了这个锁。而实线部分我的做法是获取超时锁后就释放掉,然后重新获取锁。两者应该区别不是太大,都是通过getset解决超时锁只进行一次删除的问题。
最后,依然会有个悬而未决的问题,最初获取这个锁的线程或进程,如果在超时后,锁被其他线程或者进程重新获取后,这个线程或者进程如果复活了,也会再次触发释放锁(del)?
所以我认为,del操作应该增加类似版本号的判断,本文例子用它的时间戳即可
2017年2月7日更正:
由于setnx不能设置过期时间,所以通过setnx结合expire的方式在图1中就能实现锁的过期释放策略:
if(setnx(a,'tag') =ok){
expire a 1;
}
不过set a tag ex 1 nx就已经实现了setnx+expire这两步操作,设值返回ok,已经存在返回null。由于是一个redis命令所以是原子操作。而且未来的redis版本可能去掉setnx和setex, 因此推荐用set ex nx的方式。当然为了防止假死的线程复活后删除锁,在set 的时候依然可以通过增加本版本号或者使用随机数的方式来处理。