先把结论抛出来:redis无法正确实现分布式锁!即使是redis单节点也不行!redis的所谓分布式锁无法用在对锁要求严格的场景下,比如:同一个时间点只能有一个客户端获取锁。
首先来看下单节点下一般redis分布式锁的实现,其实就是个set:
加锁:
/** * 尝试获取分布式锁 * @param jedis Redis客户端 * @param lockKey 锁 * @param requestId 请求标识 * @param expireTime 超期时间 * @return 是否获取成功 */ public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) { String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); if (LOCK_SUCCESS.equals(result)) { return true; } return false; }
可以看到,加锁其实就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time),这个set()方法一共有五个参数:
(1)第一个为key,我们使用key来当锁,因为key是唯一的。
(2)第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。
(3)第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;
(4)第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。
(5)第五个为time,与第四个参数相呼应,代表key的过期时间
解锁:
/** * 释放分布式锁 * @param jedis Redis客户端 * @param lockKey 锁 * @param requestId 请求标识 * @return 是否释放成功 */ public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId)); if (RELEASE_SUCCESS.equals(result)) { return true; } return false; }
解锁也很简单,只需要两行代码就搞定了!第一行代码,我们写了一个简单的Lua脚本代码,第二行代码,我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。使用Lua语言主要是确保上述操作是原子性的。
看上去似乎是完美无瑕的一种分布式锁的实现方式,我们重新看下加锁的代码:
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) { String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); if (LOCK_SUCCESS.equals(result)) { return true; } return false; }
场景1:
线程1在执行set的时候,redis服务端已经执行成功,但是因为网络原因,响应还没有返回给客户端,过了expireTime时间以后,响应终于回来了,对于线程1来说,它是拿到了分布式锁的,但是注意,此时的锁已经是失效的了!如果此时又来个线程2申请加锁,显然也能获取锁,因为线程1的锁已经失效了,此时就会有2个线程同时获取锁!
场景2:
线程1执行完set以后,redis服务端执行成功,在执行if的时候,jvm发生了FullGC,应用暂停,超过了expireTime以后,GC完成,程序继续执行,此时线程1仍然认为自己是持有锁的,实际上锁已经过期了!如果此时线程2又来申请加锁,成功,此时线程2也获得了锁,因此也会出现2个线程同时执行被锁保护的代码的情况!
综上,可以看出来,就算是在单节点情况下,redis也是无法实现严格意义上的分布式锁的!
如果想要实现严格意义上的分布式锁呢?最常用的就是zookeeper了。我们来看下zookeeper为啥可以实现分布式锁。
zookeeper实现分布式锁的步骤:
假设锁空间的根节点为/lock:
(1)客户端连接zookeeper,并在/lock下创建临时的且有序的子节点,第一个客户端对应的子节点为/lock/lock-0000000000,第二个为/lock/lock-0000000001,以此类推。
(2)客户端获取/lock下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁,否则监听/lock的子节点变更消息,获得子节点变更通知后重复此步骤直至获得锁;
(3)执行业务代码;
(4)完成业务流程后,删除对应的子节点释放锁。
上面的步骤可以看出来,zookeeper跟redis不一样,它是完全不依赖客户端的状态的,因此zookeeper才可以严格实现分布式锁!
redis的分布式锁是不是就一无是处了呢?当然不是!在一些要求不是那么严格的场景下还是可以使用的,比如:凌晨1点执行定时任务出报表,哪怕是执行2次也没什么问题。
参考文献:
https://www.cnblogs.com/linjiqin/p/8003838.html
http://zhangtielei.com/posts/blog-redlock-reasoning.html
https://blog.csdn.net/qiangcuo6087/article/details/79067136