我们在处理业务的时候,有需要控制串行处理的情况,如果是同一个jvm,
我们可以通过java的并发关键字 synchronized 来控制,
但是,目前我们的系统基本都是分布式系统,同一个服务是多台机器同时提供服务的。
这种情况下,我们就可以使用Redis分布式锁;
Redis中有个方法是setnx
SETNX
SETNX key value
将 key 的值设为 value ,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不做任何动作。
SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。
可用版本:>= 1.0.0时间复杂度:O(1)返回值:设置成功,返回 1 。设置失败,返回 0 。
我们在代码中使用方式
//尝试设置cachekey看是否能设置成功,设置成功,返回 1 。设置失败,返回 0
long cnt = CacheUtils.setnx(cachekey, value, 60 * 60);
//如果设置失败 则直接退出
if (cnt == 0) {
return ;
}
//执行你的业务代码
// Your Business Code
//最后退出的时候,将这个cachekey 删掉,让下一个业务处理单元可以执行该代码
CacheUtils.del(cachekey);
事情到此还没有结束。
我们在业务中用了这个方式。但是,发现不起作用。我把我们的业务代码贴出来,让大家看问题出在哪儿。
try {
//锁住 cachekey
long cnt = CacheUtils.setnx(cachekey, tableName, 60 * 60);
if (cnt == 0) {
return result;
}
// 业务代码 Business Code
// ......
} catch (Exception e) {
LOGGER.error("获取seq异常{}",e);
result = null;
} finally {
CacheUtils.del(cachekey);
}
看出来问题在哪儿了吗?
分析了以后,我们才认识到。是finally在作怪,我把修改以后的代码贴出来
boolean lockedBySelf = false;
try {
if (isLockedByOther(cacheKey,tableName)) {
return result;
}
lockedBySelf = true;
// 业务代码 Business Code
// ........
} catch (Exception e) {
LOGGER.error("获取seq异常{}",e);
result = null;
} finally {
if(lockedBySelf){
CacheUtils.del(cacheKey);
}
}
private static final boolean isLookedByOther(String cachekey, String keyName){
return 0 == CacheUtils.setnx(cachekey, keyName, 60 * 60);
}
还没看出问题的,就往下看
由于有问题的第一段代码中使用了try-catch-finally 结构
这种结构的特点是: finally 中的代码必然会被执行。
那么问题来了。
当有三台机器同时并发处理的时候,由于其中一个通过setnx 执行成功了,进入逻辑处理阶段。
另外两个当中的一个 进入了try块,通过setnx执行失败,于是退出了,
但是因为已经进入了try块 ,finally 中的代码必然会被执行
( CacheUtils.del(cachekey) 这条语句被顺带执行了)。
剩下来的那个,又可以稳稳当当,好像没有人跟他并发一样将事情顺其自然地给做了。
后面那段代码就是在finally中 判断一下,当前的key是不是被自己锁的,
如果是自己锁的,我才能解锁,否则,没门
注:有一种情况,当使用setnx时,没执行到删除锁操作时,
出现服务异常退出,会导致锁程序去除不掉,当然可以设置过期时间,
当出现这种情况交给redis自己解决,
还有种特殊情况当你设置过期时间操作出现异常退出,
这样就会被永远锁住(生产出现过),
由于之前没用预案不得不手动清理,所以要有定时清理机制,
定时清理会出现并发情况,
最终方案:定时设置过期时间,或者当长时间取不到锁设置最长获取不到锁时间,
通知运维手动清除