目录
一、使用set key value ex time nx 命令
二、使用set key value ex time nx加(UUID作为不同线程的标识) 命令
一、使用set key value ex time nx 命令
通过redisTempate的opsForValue方法设置key值,并以当前的线程id作为值
public static final String KEY_PREFIX="lock:";
public boolean getLock(long timeoutSec) {
long id = Thread.currentThread().getId();
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(name,id+"", timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(result);
}
public void unLock() {
redisTemplate.delete(KEY_PREFIX+name);
}
这种方式能够解决一些情况下的线程安全问题,但是可能会有新的问题。
二、使用set key value ex time nx加(UUID作为不同线程的标识) 命令
在集群模式下,会有多个jvm,很可能会出现线程id冲突的情况,需要加入UUID来更好的区分不同服务的线程。我用的是hutool工具生成的UUID。
public static final String KEY_PREFIX="lock:";
public static final String ID_PREFIX= UUID.randomUUID().toString(true)+"-";
public boolean getLock(long timeoutSec) {
String threadId = ID_PREFIX+Thread.currentThread().getId();
Boolean result = redisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(result);
}
public void unLock() {
redisTemplate.delete(KEY_PREFIX+name);
}
当这个redis的key时间设置过短,导致过期时间到了自动删除。但是如果这时业务还没有执行完,新的进程会进来。当业务执行完毕后,会删除新线程的key值也就是释放的不是自己线程的锁。这就要使用第三种方式来改进。
三、解决Redis锁的误删问题
在释放锁之前判断是不是当前线程所创建的锁。
public void unLock() {
String threadId = ID_PREFIX+Thread.currentThread().getId();
String id = (String)redisTemplate.opsForValue().get(KEY_PREFIX + name);
if(threadId.equals(id)) {
redisTemplate.delete(KEY_PREFIX + name);
}
}
这种解决还是不够完美,因为可能有一种很小的概率。当一个线程进入判断后准备释放锁的时候,遇到了线程阻塞,(jvm的GC机制),而在阻塞时间的过程中,触发了锁的超时释放。这时另一个线程拿到了锁,但是第一个线程阻塞完毕,释放锁,结果就是把第二个线程的锁给释放了,第二个业务还没结束,这时第三个线程又进来,导致并发问题。
产生的原因是,判断和删除这两个操作可以被打断,想要不被打断就要让这两个指令为一个原子操作。这里我用到的是lua脚本解决
四、Lua脚本解决原子问题
unlock.lua
Lua脚本在Redis中执行具有一定的原子性。
if(redis.call('get',KEY[1])==AVG[1]) then
return redis.call('del',KEY[1])
end
return 0
public void unLock() {
String threadId = ID_PREFIX+Thread.currentThread().getId();
String id = (String)redisTemplate.opsForValue().get(KEY_PREFIX + name);
redisTemplate.execute(UNLOCKSCRIPT, Collections.singletonList(id),threadId);
}
以上的解决方案可以解决大多数的问题,比如满足了互斥,安全性,高可用性等。
但是还有一些问题,比如不可重入,同一个线程无法多次获取同一个锁
不可重试,获取锁时只发送一次请求就结束了,没有重试机制
超时释放,可能出现业务还没执行完,锁就释放的情况
主从一致性,当redis集群配置了主从一致,主节点宕机,新的从节点变为了主节点。但是主节点的锁数据还没有同步到从节点,导致新的线程进入拿到锁,导致并发问题。
解决办法就是使用Redisson框架,具体看这篇文章