最初原型
使用setnx (set if not exists)上锁(设置一个key value),待程序执行结束完成之后使用del 释放这个锁(删除 key)
死锁
如果逻辑执行到中间出现异常,会导致del没有调用,所以这把锁就会一直在redis中,陷入死锁
- 解决方案:设置过期时间
expire yourKey 5 # 过期5s
早期的版本 setnx 和 expire不是原子性操作
但是在Redis 2.8版本中,作者加入了set指令的拓展参数, setnx 和 expire可以一起执行
set yourKey true ex 5 nx
超时问题
如果在加锁和释放锁之间的逻辑执行得太长,A线程的锁被redis过期指定释放锁,这时候如果B重新上锁,A代码逻辑执行结束,这时候执行释放锁的指令,就会出现A线程释放B线程的锁
为了避免这个问题,Redis分布式锁不需要用于较长的时间的任务。
另一个方案是将set指令的value参数设置为一个随机数,释放锁时先匹配随机数释放一致,然后再删除value,由于get锁匹配和del不是原子性的,redis也没有针对这两个的操作的单个原子指令,所以要使用lua脚本才能完整的实现这一功能
如果超时问题是因为A线程执行时间过长,可以考虑在持有锁的情况下再次请求加锁,如果一个锁支持同一个线程多次加锁,这就是锁的可重入。* Redis分布式锁如果要支持可重入,需要对客户端的set方法进行包装,使用线程的Threadlocal变量存储当前持有锁的计数
java代码实现
约定一些属性 获取连接
private volatile Jedis jedis;
//释放锁的关键value
private volatile String unlockValue;
public LockBase redisCil() {
if (jedis == null) {
log.info("redisCil is init");
jedis = RedisCilBase.redisCil();
return this;
}
return this;
}
获取锁的方法,这里用到set nx ex 的混合指令
public Boolean lockOn(Object key, Integer time, Object value) {
String keyStr = key.toString();
String valueStr = value.toString();
String valueRe = jedis.get(keyStr);
log.info("redis valueRe:[{}]", valueRe);
String result = jedis.set(keyStr, valueStr, SetParams.setParams().ex(time).nx());
if (StringUtils.isEmpty(result)) {
return false;
}
//上锁成功,放置一个解锁的value
unlockValue = valueStr;
return true;
}
释放锁
public Boolean unlock(Object key) {
/*这里由于匹配value和删除key不是一个原子操作,这里比较安全的方法就是使用lua脚本*/
String keyStr = key.toString();
String valueStr = jedis.get(keyStr);
if (StringUtils.isEmpty(valueStr)) {
return true;
}
if (!valueStr.equals(unlockValue)) {
return false;
}
return jedis.del(keyStr) == 1L;
}