Redission实现分布式锁(Redission如何通过lua脚本实现分布式锁,看门狗机制)

109 阅读7分钟

通过lock()获取分布式锁

调用lock()方法(获取锁没有超时时间)

关键流程

1:调用tryAcquire()方法,返回ttl;(通过目录跳转到tryAcquire())

2:若是ttl为null,说明获取锁成功,lock()方法执行结束;

3:若是null不为空,ttl则是当前持有锁线程剩余的持有时间,本线程等待被唤醒或者等待ttl时间过去后苏醒尝试获取锁;

3.1:通过subscribe()订阅锁对应的频道,获取对应的entry;

3.2:进入默认while(true)方法中,先执行一次tryAcquire()方法(在获取entry的过程中锁可能被释放),然后执行entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);作用是阻塞等待唤醒,唤醒的情况有两种:1是锁被释放了,redis通过public通知订阅频道;2是阻塞时间到了ttl,主动苏醒;

3.3:苏醒的线程重新进入循环体,重复执行3.2,知道获取锁

3.4:执行this.unsubscribe(entry, threadId),取消订阅

    private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
        long threadId = Thread.currentThread().getId();
        Long ttl = this.tryAcquire(-1L, leaseTime, unit, threadId);
        if (ttl != null) {
            CompletableFuture<RedissonLockEntry> future = this.subscribe(threadId);
            this.pubSub.timeout(future);
            RedissonLockEntry entry;
            if (interruptibly) {
                entry = (RedissonLockEntry)this.commandExecutor.getInterrupted(future);
            } else {
                entry = (RedissonLockEntry)this.commandExecutor.get(future);
            }

            try {
                while(true) {
                    ttl = this.tryAcquire(-1L, leaseTime, unit, threadId);
                    if (ttl == null) {
                        return;
                    }

                    if (ttl >= 0L) {
                        try {
                            entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                        } catch (InterruptedException var14) {
                            InterruptedException e = var14;
                            if (interruptibly) {
                                throw e;
                            }

                            entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                        }
                    } else if (interruptibly) {
                        entry.getLatch().acquire();
                    } else {
                        entry.getLatch().acquireUninterruptibly();
                    }
                }
            } finally {
                this.unsubscribe(entry, threadId);
            }
        }
    }

tryAcquire()

获取tryAcquireAsync()返回的Future,通过get()方法等待Future执行完成,获取返回的ttl;(通过目录跳转到tryAcquireAsync())

    private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
        return (Long)this.get(this.tryAcquireAsync(waitTime, leaseTime, unit, threadId));
    }

tryAcquireAsync()

关键步骤:

1:调用tryLockInnerAsync(),通过lua脚本执行redis命令获取锁,若是leaseTime有传入值,便用传入值,没有则用默认值this.internalLockLeaseTime(默认为30秒)(目录跳转到 tryLockInnerAsync)

2:当tryLockInnerAsync()返回ttl后,ttl为null时,说明当前线程成功获取了锁(可能是首次获取,或同一线程重入),此时需要确定锁的租期如何管理。 若用户在获取锁时明确指定了租期(如lock(5, TimeUnit.SECONDS)),则将 Redisson 内部维护的internalLockLeaseTime(锁的默认租期)更新为用户指定的租期(转换为毫秒)。 此时不启动看门狗,因为用户已明确锁的生存时间,锁会在leaseTime到期后自动释放,无需自动续约。 若用户未指定租期(如直接调用lock()),则触发scheduleExpirationRenewal(threadId)启动看门狗机制。此时锁的默认租期为internalLockLeaseTime(默认 30 秒),看门狗会每 10 秒自动延长锁的过期时间,确保锁的生存时间与任务执行时间匹配(直到线程主动释放锁)。(目录跳转到scheduleExpirationRenewal())

    private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
        RFuture ttlRemainingFuture;
        if (leaseTime > 0L) {
            ttlRemainingFuture = this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        } else {
            ttlRemainingFuture = this.tryLockInnerAsync(waitTime, this.internalLockLeaseTime, TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
        }

        CompletionStage<Long> f = ttlRemainingFuture.thenApply((ttlRemaining) -> {
            if (ttlRemaining == null) {
                if (leaseTime > 0L) {
                    this.internalLockLeaseTime = unit.toMillis(leaseTime);
                } else {
                    this.scheduleExpirationRenewal(threadId);
                }
            }

            return ttlRemaining;
        });
        return new CompletableFutureWrapper(f);
    }

tryLockInnerAsync(加锁lua脚本解析)

KEY[1]:lock_name , ARGV[1]:锁持有时间 ,ARGV[2]: ThreadId

1:if ((redis.call('exists', KEYS[1]) == 0) or (redis.call('hexists', KEYS[1], ARGV[2]) == 1)) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end;

说明: 若是当前锁不存在,或者锁存在,而且持有线程是本线程(锁重入状态),调用'hincrby'指令为当前锁的重入次数加一(该命令在锁不存在的时候会创建锁),并且刷新持有时间

2: return redis.call('pttl', KEYS[1]);

说明: 若是锁被其他线程获取,则返回单签锁剩余的持有时间

    <T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        return this.evalWriteAsync(this.getRawName(), LongCodec.INSTANCE, command, 
        "if ((redis.call('exists', KEYS[1]) == 0) or (redis.call('hexists', KEYS[1], ARGV[2]) == 1))
        then redis.call('hincrby', KEYS[1], ARGV[2], 1); 
        redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; 
        return redis.call('pttl', KEYS[1]);", Collections.singletonList(this.getRawName()), new Object[]{unit.toMillis(leaseTime), this.getLockName(threadId)});
    }

scheduleExpirationRenewal()

1:若是oidEntry != null 说明当前加锁状态是重入锁,实现看门狗机制的定时器已经存在

2:当前线程第一次获取锁,执行this.renewExpiration()创建实现看门狗机制的定时器;(目录跳转方法renewExpiration())

    protected void scheduleExpirationRenewal(long threadId) {
        ExpirationEntry entry = new ExpirationEntry();
        ExpirationEntry oldEntry = (ExpirationEntry)EXPIRATION_RENEWAL_MAP.putIfAbsent(this.getEntryName(), entry);
        if (oldEntry != null) {
            oldEntry.addThreadId(threadId);
        } else {
            entry.addThreadId(threadId);

            try {
                this.renewExpiration();
            } finally {
                if (Thread.currentThread().isInterrupted()) {
                    this.cancelExpirationRenewal(threadId);
                }

            }
        }

    }

renewExpiration()

关键点: 1:ee是从全局映射EXPIRATION_RENEWAL_MAP中获取的ExpirationEntry实例。若ee == null:说明锁已被释放(或从未被持有),无需创建续约任务,直接退出。 若ee != null:说明锁仍被持有,需要创建定时续约任务。

依旧持有锁的情况下继续执行下面的点:

2: Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {...},this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS),创建一个定时task,时间是默认持有时间的三分之一,也就是这是一个在10秒后执行内部任务的定时器;

3:定时任务:1):获取ent,在10秒内,锁可能已被释放(EXPIRATION_RENEWAL_MAP中对应的条目被移除),或ExpirationEntry的状态已发生变化(如线程释放锁后threadIds为空)。因此需要再次获取ent,确保基于最新的锁状态执行续约。2):若是锁没有释放,执行renewExpirationAsync(threadId)方法为当前锁进行续约。3)若是抛出异常,日志打印错误信息,同时移除这个entry;若是加锁成功,循环调用renewExpiration()方法,重新构建一个10秒的定时器,由此实现了自动续约的看门狗机制;若是加锁失败,说明当前线程已经释放锁,执行cancelExpirationRenewal((Long)null);释放定时器。(renewExpirationAsync目录跳转)

    private void renewExpiration() {...},
        ExpirationEntry ee = (ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
        if (ee != null) {
            Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
                public void run(Timeout timeout) throws Exception {
                    ExpirationEntry ent = (ExpirationEntry)RedissonBaseLock.EXPIRATION_RENEWAL_MAP.get(RedissonBaseLock.this.getEntryName());
                    if (ent != null) {
                        Long threadId = ent.getFirstThreadId();
                        if (threadId != null) {
                            CompletionStage<Boolean> future = RedissonBaseLock.this.renewExpirationAsync(threadId);
                            future.whenComplete((res, e) -> {
                                if (e != null) {
                                    RedissonBaseLock.log.error("Can't update lock {} expiration", RedissonBaseLock.this.getRawName(), e);
                                    RedissonBaseLock.EXPIRATION_RENEWAL_MAP.remove(RedissonBaseLock.this.getEntryName());
                                } else {
                                    if (res) {
                                        RedissonBaseLock.this.renewExpiration();
                                    } else {
                                        RedissonBaseLock.this.cancelExpirationRenewal((Long)null);
                                    }

                                }
                            });
                        }
                    }
                }
            }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
            ee.setTimeout(task);
        }
    }

renewExpirationAsync()

lua脚本解析: 若是当前锁存在,而且持有的线程是本身,则使用命令'pexpire'重新设置持有时间(30s)并且返回1,若不是则返回0

    protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
        return this.evalWriteAsync(this.getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
        "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('pexpire', KEYS[1], ARGV[1]); return 1; end; 
        return 0;",
        Collections.singletonList(this.getRawName()), this.internalLockLeaseTime, this.getLockName(threadId));
    }

调用tryLock()方法(获取锁有超时时间)

关键点: 1:初始化参数并尝试获取锁,ttl == null:当前线程成功获取到锁(首次获取或重入),直接返回true。 ttl != null:锁被其他线程持有,ttl为锁的剩余过期时间(单位:毫秒),需进入等待逻辑。(尝试获取锁的方法tryAcquire()与lock()中的一样,详细实现看lock()中的tryAcquire()目录)

2:若首次尝试获取锁失败,且已消耗完所有等待时间(time <= 0),则直接返回false,否则进入循环常识的逻辑

3:subscribe(threadId):为当前线程订阅锁释放的通知频道(如redisson_lock__channel:{lockName}),返回的subscribeFuture会在订阅完成后得到RedissonLockEntry(封装订阅状态和阻塞工具)。

4:subscribeFuture.get(time, TimeUnit.MILLISECONDS)阻塞等待订阅完成,订阅操作本身也有超时限制(使用剩余的time),若订阅超时或失败,直接返回false,并清理订阅资源。

5:订阅成功后,进入循环,在剩余等待时间内反复尝试获取锁。1)每次循环先尝试获取锁(tryAcquire),若成功直接返回。2)若失败,根据锁的剩余时间(ttl)和剩余等待时间,决定线程阻塞的时长(通过latch.tryAcquire):若ttl更小(锁即将释放),则阻塞ttl毫秒(锁释放后会被通知唤醒,减少无效等待)。若剩余等待时间更小,则阻塞剩余时间(避免超过总等待时间)。3)阻塞结束后,更新剩余等待时间,继续循环,直到超时

    public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
        long time = unit.toMillis(waitTime);
        long current = System.currentTimeMillis();
        long threadId = Thread.currentThread().getId();
        Long ttl = this.tryAcquire(waitTime, leaseTime, unit, threadId);
        if (ttl == null) {
            return true;
        } else {
            time -= System.currentTimeMillis() - current;
            if (time <= 0L) {
                this.acquireFailed(waitTime, unit, threadId);
                return false;
            } else {
                current = System.currentTimeMillis();
                CompletableFuture<RedissonLockEntry> subscribeFuture = this.subscribe(threadId);

                try {
                    subscribeFuture.get(time, TimeUnit.MILLISECONDS);
                } catch (TimeoutException var21) {
                    if (!subscribeFuture.completeExceptionally(new RedisTimeoutException("Unable to acquire subscription lock after " + time + "ms. Try to increase 'subscriptionsPerConnection' and/or 'subscriptionConnectionPoolSize' parameters."))) {
                        subscribeFuture.whenComplete((res, ex) -> {
                            if (ex == null) {
                                this.unsubscribe(res, threadId);
                            }

                        });
                    }

                    this.acquireFailed(waitTime, unit, threadId);
                    return false;
                } catch (ExecutionException var22) {
                    this.acquireFailed(waitTime, unit, threadId);
                    return false;
                }

                try {
                    time -= System.currentTimeMillis() - current;
                    if (time <= 0L) {
                        this.acquireFailed(waitTime, unit, threadId);
                        boolean var24 = false;
                        return var24;
                    } else {
                        boolean var16;
                        do {
                            long currentTime = System.currentTimeMillis();
                            ttl = this.tryAcquire(waitTime, leaseTime, unit, threadId);
                            if (ttl == null) {
                                var16 = true;
                                return var16;
                            }

                            time -= System.currentTimeMillis() - currentTime;
                            if (time <= 0L) {
                                this.acquireFailed(waitTime, unit, threadId);
                                var16 = false;
                                return var16;
                            }

                            currentTime = System.currentTimeMillis();
                            if (ttl >= 0L && ttl < time) {
                                ((RedissonLockEntry)this.commandExecutor.getNow(subscribeFuture)).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                            } else {
                                ((RedissonLockEntry)this.commandExecutor.getNow(subscribeFuture)).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
                            }

                            time -= System.currentTimeMillis() - currentTime;
                        } while(time > 0L);

                        this.acquireFailed(waitTime, unit, threadId);
                        var16 = false;
                        return var16;
                    }
                } finally {
                    this.unsubscribe((RedissonLockEntry)this.commandExecutor.getNow(subscribeFuture), threadId);
                }
            }
        }
    }

unlock()

关键方法:unlockInnerAsync

LUA脚本说明: 1:若是锁不存在或者持有锁的线程不是本线程返回nil; 2:若是线程持有锁,线程重复次数减一,并且返回当前重入次数counter 3:若counter>0,线程不释放锁,刷新锁的过期时间 4:否则说明,当前线程需要释放锁,同时通过命令publish通知订阅条目锁释放

    protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return this.evalWriteAsync(this.getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
        "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil;end; 
        local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); 
        if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); return 0; 
        else redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end; 
        return nil;", 
        Arrays.asList(this.getRawName(), this.getChannelName()), new Object[]{LockPubSub.UNLOCK_MESSAGE, this.internalLockLeaseTime, this.getLockName(threadId)});
    }