Redis实现的分布式锁被大家广泛用于解决在分布式环境下的并发问题——使用set NX EX,当某一个key存在时,返回失败,当key不存在时,设置新值和过期时间,返回成功。
那么如何通过Redis实现一个可重入的分布式锁呢?
我们可以参考一下Java中ReentrantLock的实现,在持有锁时记录下线程信息,获取锁时检查线程id是否相同,那么在Redis中也可参考相同实现:
此时就出现了并发问题,需要先比较在更新,Redis并未提供CAS原子性命令,需要借助Redis的其它特性解决,下面为大家介绍两种方法。
redis支持了简单的事务,提供了以下几个命令:
通过watch命令,实现对某些Key的监听,当一个事务提交时,会优先检测监听的key是否发生改变,如果已发生改变,取消事务,若并未改变,执行事务。
public boolean tryLock(String key, int timeout, String threadId){
Transaction multi = null;
try {
// 监控key
jedis.watch(key);
// 获取锁信息
String lock = jedis.get(key);
// 已持有锁且不是当前线程,获取锁失败
if (StringUtils.isNotEmpty(lock) && !lock.equals(threadId)) {
return false;
}
// 开启事务
multi = jedis.multi();
// 添加命令
multi.setex(key, timeout, threadId);
// 执行事务
multi.exec();
return true;
} catch (Exception e) {
if (Objects.nonNull(multi)) {
multi.discard();
}
return false;
} finally {
jedis.unwatch();
}
}
注意:
redis中存在一个字典,用于保存所有被监视的key和其对应监视的客户端列表,字典的键是被监视的key,而值则是监视其的客户端链表。
Lua是一种小巧的脚本语言,redis提供了对lua脚本执行的能力。通过将多个请求通过脚本的形式一次发送,redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。
EVAL script numkeys key [key …] arg [arg …]
向redis发送具体的脚本内容和参数,完成脚本的执行。
// 预加载脚本,返回其对应的sha1值
SCRIPTLOCAD script
// 提交脚本对应的sha1值和参数,执行脚本
EVALSHA sha1 numkeys key [key …] arg [arg …]
redis具有缓存能力,提前将脚本语句在redis缓存,后续只需发送脚本对应的sha1值,有效的减少带宽的消耗。
public class QueueStateOperateClient {
private static final Object LOCK_OBJECT = new Object();
private static volatile String lockSha;
private static volatile String unLockSha;
public static Response lock(Pipeline pipeline, String key) {
// 脚本检查
preCheck();
return pipeline.evalsha(lockSha, 2, new String[]{setSuffix(key), setSuffix(QueueConstant.LOCAL_HOST)});
}
public static Response unlock(Pipeline pipeline, String key) {
preCheck();
return pipeline.evalsha(unLockSha, 2, new String[]{setSuffix(key), setSuffix(QueueConstant.LOCAL_HOST)});
}
public static void preCheck() {
// 脚本是否已经加载
if (StringUtils.isNotBlank(lockSha) && StringUtils.isNotBlank(unLockSha)) {
return;
}
synchronized (LOCK_OBJECT) {
if (StringUtils.isNotBlank(lockSha) && StringUtils.isNotBlank(unLockSha)) {
return;
}
// 加载script
SuishenRedisTemplate queueRedisTemplate = (SuishenRedisTemplate) SourceEventQueueManager
.getApplicationContext().getBean("queueRedisTemplate");
new SuishenRedisExecutor().exe(jedis -> {
lockSha = jedis.scriptLoad(getLockScript());
unLockSha = jedis.scriptLoad(getUnLockScript());
return true;
}, queueRedisTemplate);
}
}
private static String getLockScript() {
return "local ip = redis.call(\"get\",KEYS[1]);" +
"if (not ip) or ip==KEYS[2] " +
"then " +
" return redis.call(\"setex\",KEYS[1],10,KEYS[2]);" +
"else " +
" return \"FAIL\"" +
"end ";
}
private static String getUnLockScript() {
return "local ip = redis.call(\"get\",KEYS[1]);" +
"if ip and ip==KEYS[2] " +
"then " +
" redis.call(\"del\",KEYS[1]);" +
"end " +
"return \"OK\"";
}
/**
* 集群redis设置分片规则
*
* @param key
* @return
*/
private static String setSuffix(String key) {
return key + "{queue}";
}
}
对于分布式redis,执行lua时,需要保证所有的key均在同一分片下才可正确的执行,当key中存在{}时,分布式redis只会对{}中的字符进行分片规则计算,通过这种方式,可以保证不同的key均在同一分片下。
使用EVALSHA时,如果当前sha1对应的脚本在redis中不存在时,会抛出此异常,常见的场景:
当发现此异常时,需要重新SCRIPTLOAD脚本,加入redis缓存。