项目里面由于之前使用了RedisLockRegistry作为分布式锁。
但是某些任务由于设计不合理,执行任务时间过长,超过一定时间后导致锁的释放失败。
经过排查看到源码如下。
获取锁的方法
private boolean obtainLock() {
Boolean success =
RedisLockRegistry.this.redisTemplate.execute(RedisLockRegistry.this.obtainLockScript,
Collections.singletonList(this.lockKey), RedisLockRegistry.this.clientId,
String.valueOf(RedisLockRegistry.this.expireAfter));
boolean result = Boolean.TRUE.equals(success);
if (result) {
this.lockedAt = System.currentTimeMillis();
}
return result;
}
最后看到也是通过lua脚本来执行。
private static final String OBTAIN_LOCK_SCRIPT =
"local lockClientId = redis.call('GET', KEYS[1])\n" +
"if lockClientId == ARGV[1] then\n" +
" redis.call('PEXPIRE', KEYS[1], ARGV[2])\n" +
" return true\n" +
"elseif not lockClientId then\n" +
" redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2])\n" +
" return true\n" +
"end\n" +
"return false";
该脚本的意思如下:
lockClientId 为keys[1]对应的value 这里的keys[1]其实就是锁的名字
随后分3种情况
1.锁对应的客户端id存在,如果拿到该锁的客户端还是一样,说明是重入锁。------重入
2.锁对应的客户id不存在,直接上锁,设置lockname--->clientId的键值对------上锁
3.锁对应的客户id存在,且不等于当前传入的客户端id,说明锁已经被占用了,直接返回false---获取锁失败
那么再回头看看之前的方法
private boolean obtainLock() {
Boolean success =
RedisLockRegistry.this.redisTemplate.execute(RedisLockRegistry.this.obtainLockScript,
Collections.singletonList(this.lockKey), RedisLockRegistry.this.clientId,
String.valueOf(RedisLockRegistry.this.expireAfter));
boolean result = Boolean.TRUE.equals(success);
if (result) {
this.lockedAt = System.currentTimeMillis();
}
return result;
}
看到对应的过期时间,其实就是RedisLockRegistry的expireAfter。
public final class RedisLockRegistry implements ExpirableLockRegistry, DisposableBean {
private static final long DEFAULT_EXPIRE_AFTER = 60000L;
说明锁的默认时间就是60秒。
那么这种操作会有什么问题。
说白了就是超过60秒之后,redis对应的lockname--->client键值对就消失了。
对于单个服务,单个redis的场景来说,不会出现问题。
源码如下
public final class RedisLockRegistry implements ExpirableLockRegistry, DisposableBean {
private final class RedisLock implements Lock {
private final ReentrantLock localLock = new ReentrantLock();
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
long now = System.currentTimeMillis();
if (!this.localLock.tryLock(time, unit)) {
return false;
}
try {
long expire = now + TimeUnit.MILLISECONDS.convert(time, unit);
boolean acquired;
while (!(acquired = obtainLock()) && System.currentTimeMillis() < expire) { //NOSONAR
Thread.sleep(100); //NOSONAR
}
if (!acquired) {
this.localLock.unlock();
}
return acquired;
}
catch (Exception e) {
this.localLock.unlock();
rethrowAsLockException(e);
}
return false;
}
}
}
从上面的代码来看,是一个双重锁机制。
可以减少对redis的访问,如果在单实例的情况下,哪怕redis中的键值对已经过期了,其他任务也无法拿到该锁,因为最外层的锁拿不到,保证了哪怕任务的执行时间>lockname->clientid键值对的存活时间,也可以保证同一时刻只有一个任务在执行(仅仅是单实例)。
跟redisson不一样,redisson是不断地去延长键值对的存活时间,而RedisLockRegistry通过最外层的锁,但是对于多实例的情况下,就会有问题了。所以RedisLockRegistry这个组件,只适用于单实例的情况。
另外如果任务时间过长,也会出现问题。
代码如下
@Override
public void unlock() {
if (!this.localLock.isHeldByCurrentThread()) {
throw new IllegalStateException("You do not own lock at " + this.lockKey);
}
if (this.localLock.getHoldCount() > 1) {
this.localLock.unlock();
return;
}
try {
重点---任务执行过长,可能会导致锁无法释放
if (!isAcquiredInThisProcess()) {
throw new IllegalStateException("Lock was released in the store due to expiration. " +
"The integrity of data protected by this lock may have been compromised.");
}
if (Thread.currentThread().isInterrupted()) {
RedisLockRegistry.this.executor.execute(this::removeLockKey);
}
else {
removeLockKey();
}
if (logger.isDebugEnabled()) {
logger.debug("Released lock; " + this);
}
}
catch (Exception e) {
ReflectionUtils.rethrowRuntimeException(e);
}
finally {
this.localLock.unlock();
}
}
public boolean isAcquiredInThisProcess() {
任务过久的话,redis里面lockname->clientId这个键值对已经消失了。所以会报错,导致释放锁失败。
return RedisLockRegistry.this.clientId.equals(
RedisLockRegistry.this.redisTemplate.boundValueOps(this.lockKey).get());
}
修改一下工具类,对于Unlock的时候做一个简单的tryCatch即可
private static RedisLockRegistry REDISLOCKREGISTRY_STATIC;
private static String LOCK = "redisUtilslock";
public static boolean lock(Runnable codes, String lockKey) {
if (REDISLOCKREGISTRY_STATIC == null) {
synchronized (LOCK) {
REDISLOCKREGISTRY_STATIC = ApplicationContextUtils.getBean(RedisLockRegistry.class);
}
}
Lock lock = REDISLOCKREGISTRY_STATIC.obtain(lockKey);
if (!lock.tryLock()) {
return false;
}
try {
codes.run();
} finally {
try {
捕获对应的异常,避免任务执行过长,导致无法释放锁。
lock.unlock();
}catch(Exception e) {
log.info("Lock was released in the store due to expiration");
}
}
return true;
}