当前位置: 首页>后端>正文

RedisLockRegistry超时释放问题

项目里面由于之前使用了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;
    }

https://www.xamrdz.com/backend/3md1934977.html

相关文章: