基于redis的分布式锁项目肯定经常用到,主要是为了避免重复处理,或者由于并发带来的脏数据或者错误的处理。
使用锁就必须注意一下几点:
1、互斥,同一时间不能有多个client能获取到锁
2、不能发生死锁,不能因为有锁的client因为崩溃或者解锁命令请求失败导致无法释放锁
3、自己只能解自己上的锁,不能删除别人的锁
下面介绍一下代码为什么这么写
1、为什么不用setnx和expire两个命令来实现加锁,因为如果再执行setnx后加过期时间崩溃了,就无法解锁。
2、为什么需要使用随机字符串来做lockvalue,主要是为了防止不同client的锁都是一样的,防止client误删
3、为什么还需要Lua脚本代码来实现删除锁,纵然我们在删除之前比对了锁,但是get和del之间不是原子性的,所以也防止删除别人的锁,用lua脚本代码实现原子操作
下面是一个不侵占业务的通过redis分布式锁来执行的方法代码,当然这里只是参考,一种解决问题的方案,至于业务实现怎么写,大家可以随便发挥。
/**
* 分布式锁
*
* @param r 继承于Redis
* @param lockKey 分布式锁的key
* @param voidMethod 获得到锁后需要执行的方法
* @param expireSeconds 锁定时间 单位/s(需要评估方法执行时间)
* @param retrySeconds 重试间隔 单位/s
* @throws ServiceException
*/
public <R extends Redis> void lock(R r, String lockKey, VoidMethodInterface voidMethod, int expireSeconds, int retrySeconds) throws ServiceException {
long begin = 0L;
long retryMills = retrySeconds * 1000;
//获取随机字符串,避免lockValue相同
String lockValue = UUID.randomUUID().toString();
while (begin <= retryMills) {
String response = r.set(lockKey, lockValue, "nx", "ex", expireSeconds);
if (response != null) {
logger.debug("lock method exec begin");
try {
voidMethod.exec();
} catch (Exception e) {
logger.error("method exec failed ,msg:%s", e.getMessage());
} finally {
String value = r.get(lockKey);
//当缓存里还有该key对应的值时,才去删除锁,避免执行时间过长导致锁被释放
if (!StringUtils.isEmpty(value) && lockValue.equals(value)) {
// 避免若在此时,这把锁突然不是这个客户端的,则会误解锁
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
r.eval(script, Collections.singletonList(lockKey), Collections.singletonList(lockValue));
}
}
logger.debug("lock method exec success");
return;
} else {
long waitMills = random.nextInt(WAIT_INTERVAL_MIN_MILLS, WAIT_INTERVAL_MAX_MILLS);
try {
Thread.sleep(waitMills);
} catch (InterruptedException ex) {
throw new UnexpectedStateException(ex);
}
begin = begin + waitMills;
logger.debug("等待获取锁,当前等待时间:%sms", begin);
}
}
logger.error("等待获取锁超时");
}