分布式锁之redisson

使用redisson的原因

  • 比zk性能要好
  • 比使用redis.setnx的优势
    • 使用redis.setnx时,如果系统挂掉,会一直锁住
    • redisson将ttl和set 通过lua脚本原子性批量执行

实现

Lock lock = redisson.getLock("test");
lock.lock();

源码

RLock锁API

public interface RLock {
    //----------------------Lock接口方法-----------------------

    /**
     * 加锁 锁的有效期默认30秒
     */
    void lock();
    /**
     * tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false .
     */
    boolean tryLock();
    /**
     * tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,
     * 在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
     *
     * @param time 等待时间
     * @param unit 时间单位 小时、分、秒、毫秒等
     */
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    /**
     * 解锁
     */
    void unlock();
    /**
     * 中断锁 表示该锁可以被中断 假如A和B同时调这个方法,A获取锁,B为获取锁,那么B线程可以通过
     * Thread.currentThread().interrupt(); 方法真正中断该线程
     */
    void lockInterruptibly();

    //----------------------RLock接口方法-----------------------
    /**
     * 加锁 上面是默认30秒这里可以手动设置锁的有效时间
     *
     * @param leaseTime 锁有效时间
     * @param unit      时间单位 小时、分、秒、毫秒等
     */
    void lock(long leaseTime, TimeUnit unit);
    /**
     * 这里比上面多一个参数,多添加一个锁的有效时间
     *
     * @param waitTime  等待时间
     * @param leaseTime 锁有效时间
     * @param unit      时间单位 小时、分、秒、毫秒等
     */
    boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;
    /**
     * 检验该锁是否被线程使用,如果被使用返回True
     */
    boolean isLocked();
    /**
     * 检查当前线程是否获得此锁(这个和上面的区别就是该方法可以判断是否当前线程获得此锁,而不是此锁是否被线程占有)
     * 这个比上面那个实用
     */
    boolean isHeldByCurrentThread();
    /**
     * 中断锁 和上面中断锁差不多,只是这里如果获得锁成功,添加锁的有效时间
     * @param leaseTime  锁有效时间
     * @param unit       时间单位 小时、分、秒、毫秒等
     */
    void lockInterruptibly(long leaseTime, TimeUnit unit);  
}

RedissonLock - tryAcquireAsync

private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    // 如果过期时间不为空,则直接调用tryLockInnerAsync
    if (leaseTime != -1) {
        return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    }
    //如果过期时间为空,则取internalLockLeaseTime时间,默认为30s
    //tryLockInnerAsync会通过lua脚本执行hincrby pttl等命令  
    //redissonlock 采用的是hash结构,外层的key为创建RedissonLock时指定的name
    //里层的key为线程id
    RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
        if (e != null) {
            return;
        }

        // lock acquired
        if (ttlRemaining == null) {
            //在该锁成功后,将该线程id添加至ExpirationEntry
            scheduleExpirationRenewal(threadId);
        }
    });
    return ttlRemainingFuture;
}

RedissonLock - renewExpiration - 定时续期,防止程序未执行完,redis过期解锁

private void renewExpiration() {
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (ee == null) {
        return;
    }
    // 每1/3过期时间执行一次任务
    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
            ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
            if (ent == null) {
                return;
            }
            //获取到第一个线程id
            Long threadId = ent.getFirstThreadId();
            if (threadId == null) {
                return;
            }
            // 根据线程id续期
            RFuture<Boolean> future = renewExpirationAsync(threadId);
            future.onComplete((res, e) -> {
                if (e != null) {
                    log.error("Can't update lock " + getName() + " expiration", e);
                    EXPIRATION_RENEWAL_MAP.remove(getEntryName());
                    return;
                }

                if (res) {
                    // reschedule itself
                    renewExpiration();
                }
            });
        }
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);

    ee.setTimeout(task);
}

如果在redisson该续期时stw了,如何解决

忽略

redisson是在剩余过期时间超过1/3时续期,通常过期时间都会以s为单位,而stw时间非常短,以ms为单位,故不会出现上面的问题

RedissonRedLock

RedLock也不能解决gc的问题,只能能过多个无任何关联关系的redis服务器解决服务端挂掉的问题

zk

zk临时节点的生命周期和客户端会话绑定。也就是说,如果客户端会话失效,那么这个节点就会自动被清除掉。注意,这里提到的是会话失效,而非连接断开。另外,在临时节点下面不能创建子节点。

这里还要注意一件事,就是当你客户端会话失效后,所产生的节点也不是一下子就消失了,也要过一段时间,大概是 10 秒以内,可以试一下,本机操作生成节点,在服务器端用命令来查看当前的节点数目,你会发现客户端已经 stop,但是产生的节点还在。

看上去通过 Zookeeper 实现分布式锁还是比较好的一种解决方案,但是它是完美的吗?从上面的分布式锁的流程可知,客户端线程想要获取锁就需要创建临时节点,这个时候客户端和 Zookeeper 之间就会维护一个 session,来表示该客户端还在排队等待获取锁。因此这个方案的潜在问题就在于一旦出现网络异常,或者客户端发生 STW GC,那么就可能导致 session 关闭,从而导致临时节点被关闭,此时就会出现原来客户端持有的锁被删除了,如果有另外的客户端过来加锁的话可以成功获取,那么此时就出现并发安全问题了。因此在这种极端条件下,Zookeeper 的分布式锁实现方案也不是 100%保证安全的。