一.前提
相信大家在使用分布式锁的时候都会选择redis或者zookeeper来实现。今天我们来讲一讲使用Redis实现分布式锁里面的坑。大家要避免
二.错误案例
1.jedis.setnx方法和jedis.expire组合实现加锁
上代码:
java
Long result = jedis.setnx(lockKey, requestId);
if (result == 1) {
// 若在这里程序突然崩溃,则无法设置过期时间,将发生死锁
jedis.expire(lockKey, expireTime);
}
上述代码 如果在setnx之后程序突然崩溃,那废了,没有设置上过期时间,会产生死锁。为什么会出现问题呢,因为setnx()和expire()不具备原子性。
2.使用jedis.setnx()命令
其中key是锁,value是过期时间
java
public static boolean wrongGetLock2(Jedis jedis, String lockKey, int expireTime) {
long expires = System.currentTimeMillis() + expireTime;
String expiresStr = String.valueOf(expires);
// 如果当前锁不存在,返回加锁成功
if (jedis.setnx(lockKey, expiresStr) == 1) {
return true;
}
// 如果锁存在,获取锁的过期时间
String currentValueStr = jedis.get(lockKey);
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
// 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间
String oldValueStr = jedis.getSet(lockKey, expiresStr);
if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
// 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才有权利加锁
return true;
}
}
// 其他情况,一律返回加锁失败
return false;}
想想会出现什么问题呢? 答案就是如果分布式系统中的的多个系统时间可能不一致,比如A机器比其他机器早了一分钟,过期时间设置为一分钟,那对于其他机器来说,当A上锁的时候,其他机器并不会被影响。第二个问题锁不具备拥有者标识,即任何客户端都可以解锁。正常我们应该保证分布式锁中谁加锁谁就要解锁,你家的锁头别人的钥匙要是能开岂不是很严重。
三.正确姿势
回想以上问题,我们其实就是要保证加锁与添加过期时间需要原子性。那我们应该怎么办呢?使用Lua脚本啊
java
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;
}}
我们使用eval()命令来实现让redis执行lua脚本保证原子性。
四.解锁的错误示范
1.直接使用jedis.del()命令
还是上面说到的问题,这种不先判断锁的拥有者而直接解锁的方式,会导致任何客户端都可以随时进行解锁,即使这把锁不是它的。
2.程序突然崩溃
java
public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {
// 判断加锁与解锁是不是同一个客户端
if (requestId.equals(jedis.get(lockKey))) {
// 若在此时,这把锁突然不是这个客户端的,则会误解锁
jedis.del(lockKey);
}}
如代码注释,问题在于如果调用jedis.del()方法的时候,这把锁已经不属于当前客户端的时候会解除他人加的锁。那么是否真的有这种场景?答案是肯定的,比如客户端A加锁,一段时间之后客户端A解锁,在执行jedis.del()之前,锁突然过期了,此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了。
五.结尾
当然,我们可以使用专门的redis分布式锁组件redission.