redisson分布式锁你真的清楚吗?

101 阅读3分钟

我们在使用分布式锁的时候,Redis是首选,java封装的redisson是一个很好的使用工具类,使用也很简单。

但是他的原理你清楚吗?在这里提几个问题: 1、锁的键值是什么? 2、锁是如果保证一直有效的? 3、锁过期,锁重入有这方面的机制吗?

在这里我们就要梳理Redisson的源码来解析,通过一层一层的深入我们看源码在 从org.redisson.RedissonLock#tryLock()入手深入 第一个比较复杂的方法调用就是 org.redisson.RedissonLock#tryAcquireOnceAsync 这个方法掉了加锁方法tryLockInnerAsync,与后续处理的方法scheduleExpirationRenewal

private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    RFuture<Boolean> acquiredFuture;
    // Redis加锁处理
            // 默认leaseTime -1 不进入
    if (leaseTime > 0) {
        acquiredFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
    } else {
            // 默认进入 
        acquiredFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
                TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
    }

    CompletionStage<Boolean> f = acquiredFuture.thenApply(acquired -> {
        // lock acquired
        if (acquired) {
            // 默认leaseTime -1 不进入
            if (leaseTime > 0) {
                internalLockLeaseTime = unit.toMillis(leaseTime);
            } else {
                // 进行锁续期处理
                scheduleExpirationRenewal(threadId);
            }
        }
        return acquired;
    });
    return new CompletableFutureWrapper<>(f);
}

org.redisson.RedissonLock#tryLockInnerAsync

<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,
            "if (redis.call('exists', KEYS[1]) == 0) then " +
                    "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                    "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                    "return nil; " +
                    "end; " +
                    "if (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(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
}

这里面在加锁的的时候包含了lua脚本,我们来解析这个脚本

-- 通过上面方法的入参我们对入参进行解读
-- KEYS[1] 表示键值
-- ARGV[1] 过期时间
-- ARGV[2] 表示线程ID

-- 接借来进行代码解读

--判断键值是否存在,0表示不存在证明没有有加锁,可以正常上锁
if (redis.call('exists', KEYS[1]) == 0) then     
    --Redis Hincrby 命令用于为哈希表中的字段值加上指定增量值。如果哈希表的 key 不存在,一个新的哈希表被创建并执行 HINCRBY 命令。这里添加键值
    redis.call('hincrby', KEYS[1], ARGV[2], 1);  
    -- 这里加入锁的过期时间,防止服务宕机后,无法释放锁
    redis.call('pexpire', KEYS[1], ARGV[1]); 
    -- 加锁成功不需要等待
    return nil;
end; 
-- 判断当前键值是否存在当前线程id ,防止同一个服务的不同线程抢锁
if (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]);

通过上面我们了解到加锁的处理方式与唯一值的关系,保证了加锁的互斥性

接下来我们深入org.redisson.RedissonBaseLock#scheduleExpirationRenewal

protected void scheduleExpirationRenewal(long threadId) {
    ExpirationEntry entry = new ExpirationEntry();
    ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
    if (oldEntry != null) {
        oldEntry.addThreadId(threadId);
    } else {
        entry.addThreadId(threadId);
        try {
           // 关键方法
            renewExpiration();
        } finally {
            if (Thread.currentThread().isInterrupted()) {
                cancelExpirationRenewal(threadId);
            }
        }
    }
}

继续深入 org.redisson.RedissonBaseLock#renewExpiration

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;
            }
            Long threadId = ent.getFirstThreadId();
            if (threadId == null) {
                return;
            }
            // 真正的锁续期方法
            CompletionStage<Boolean> future = renewExpirationAsync(threadId);
            future.whenComplete((res, e) -> {
                if (e != null) {
                    log.error("Can't update lock " + getRawName() + " expiration", e);
                    EXPIRATION_RENEWAL_MAP.remove(getEntryName());
                    return;
                }
                
                if (res) {
                    // reschedule itself
                    renewExpiration();
                } else {
                    cancelExpirationRenewal(null);
                }
            });
        }
        // 表示在有效时长的1/3后执行
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
    
    ee.setTimeout(task);
}

真正的锁续期方法 org.redisson.RedissonBaseLock#renewExpirationAsync 充实锁的有效时长

protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
    return evalWriteAsync(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(getRawName()),
            internalLockLeaseTime, getLockName(threadId));
}

至此已经完成了一把分布式锁,符合互斥、可重入、防死锁的基本特点。同时解答了上面的问题。 新人编辑,自我认识,多多指教!!!