通过redisson源码看看它实现的分布式锁

redisson实现分布式锁,是通过一个hash结构存储的,形式如下:

MY_LOCK 3444e697-8ab7-43ba-bfb5-28a38aeb1f02:1 1

MY_LOCK 是我获取分布式锁的时候,通过redisson.getLock(“MY_LOCK”)定义的,它作为hash结构的key

3444e697-8ab7-43ba-bfb5-28a38aeb1f02:1 作为hash结构的一个field,冒号后面的1是线程id。

1 是field的值,作为当前线程重入锁的次数。

每次通过判断所得key和当前线程对应的field是否存在来判断是否可以获取锁。以上这些内容都可以在调试源码的过程中看到具体细节。我是从org.redisson.RedissonLock#tryLock() 作为入口,罗列了比较关键的代码:

先整体看看加锁和给锁续期:

private RFuture<Boolean> tryAcquireOnceAsync(long leaseTime, TimeUnit unit, long threadId) {
        // ......
        // 加锁方法返回异步执行的结果,结果泛型为Boolean,这个结果是通过RedisCommands.EVAL_NULL_BOOLEAN转换的,如果结果为null, 返回true,否则返回false.
        RFuture<Boolean> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
        // 给加锁返回的异步结果注册执行完成时的回调方法onComplete,是否需要给锁续期就是根据加锁结果判断的,
        ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
            if (e != null) {
                return;
            }

            // lock acquired
            // 如果加锁方法返回true, 需要通过scheduleExpirationRenewal(threadId)方法给锁续期
            if (ttlRemaining) { 
                scheduleExpirationRenewal(threadId);
            }
        });
        return ttlRemainingFuture;
    }

加锁方法内部实现

<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        internalLockLeaseTime = unit.toMillis(leaseTime);

        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
                  "if (redis.call('exists', KEYS[1]) == 0) then " +
                      "redis.call('hset', 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.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
    }

列一下我调试时这段lua的参数

  • KEYS[1]是MY_LOCK, 自定义的锁的key
  • ARG[1] 是标识线程的哈希结构的field :81c39aea-b41f-4505-ae92-0f567fdc8651:1
  • ARG[2] 是看门狗默认的超时时间
    解释下这段lua的三个分支都做了什么事情:

如果MY_LOCK不存在,则创该建哈希结构,key为MY_LOCK,field为线程标记,value为1,过期时间30秒, 返回null, 这是首次加锁的情况。前面说到返回null的时候加锁方法会转为true, 此时回调方法中会去设置定时给锁续期的事情。

如果MY_LOCK这个key存在,并且MY_LOCK的线程field的存在,则给field增加1. 重新设置MY_LOCK的超时时间30s。返回null,这是锁重入时的情况,每次重入给线程field的值加1. 此时的返回结果也转为true, 并去续期锁。

否则,就是MY_LOCK存在,但是field不存在,表明当前线程已经不持有锁了,返回key的剩余生存时间. 此时结果不为null, 返回的RFuture 结果为false. 不会再去给锁续期。

补充一些redis知识:

exists 检查给定key是否存在
hexists 检查希表 key 中,给定域 field 是否存在
pexpire 设置过期时间,单位毫秒
expire 单位秒
hincrby 对hash的field的value增加,只能操作数值型
pttl 返回给定 key 的剩余生存时间,单位毫秒
ttl 单位秒
EVAL script numkeys key [key …] arg [arg …]

  • script: 参数是一段 Lua 5.1 脚本程序。脚本不必(也不应该)定义为一个 Lua 函数。
  • numkeys: 用于指定键名参数的个数。
  • key [key …]: 从 EVAL 的第三个参数开始算起,表示在脚本中所用到的那些 Redis 键(key),这些键名参数可以在 Lua 中通过全局变量 KEYS 数组,用 1 为基址的形式访问( KEYS[1] , KEYS[2] ,以此类推)。
  • arg [arg …]: 附加参数,在 Lua 中通过全局变量 ARGV 数组访问,访问的形式和 KEYS 变量类似( ARGV[1] 、 ARGV[2] ,诸如此类)。

整体看看给锁续期

private void renewExpiration() {
        ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
        if (ee == null) {
            return;
        }
        // 创建Timeout,这里创建的是HashedWheelTimeout,这是netty的HashedWheelTimer的一个内部类,HashedWheelTimer是一个优秀的定时任务实现方案,给锁续期就是定时任务去判断的。
        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;
                }
  
              // 这里真正的去更新锁的时间的操作
                RFuture<Boolean> future = renewExpirationAsync(threadId);
                // 根据更新锁返回的结果决定是否需要继续给锁续期
                future.onComplete((res, e) -> {
                    if (e != null) {
                        log.error("Can't update lock " + getName() + " expiration", e);
                        return;
                    }
                    // 如果给锁续期返回成功,调用自己重新添加续期任务,因为之前的任务可能会过期
                    if (res) {
                        // reschedule itself
                        renewExpiration();
                    }
                });
            }
            // 定时任务的执行时机是设置的看门狗超时时间的三分之一,默认看门狗超时时间30s的话那任务就是10s执行一次。
        }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
        
        ee.setTimeout(task);
    }

看看给锁续期方法内部

protected RFuture<Boolean> renewExpirationAsync(long threadId) {
        return commandExecutor.evalWriteAsync(getName(), 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.<Object>singletonList(getName()), 
            internalLockLeaseTime, getLockName(threadId));
    }

如果MY_LOCK的线程field存在,说明当前线程还持有锁,需要重新设置过期时间为看门狗设置的时间,返回1。否则返回0.

整体看看解锁

public RFuture<Void> unlockAsync(long threadId) {
        RPromise<Void> result = new RedissonPromise<Void>();
    	//  解锁
        RFuture<Boolean> future = unlockInnerAsync(threadId);
		// 解锁完成后回调
        future.onComplete((opStatus, e) -> {
            // 如果发生了异常,取消自动续期,并组装失败结果,结束
            if (e != null) { 
                cancelExpirationRenewal(threadId);
                result.tryFailure(e);
                return;
            }
			// 如果解锁返回null,也就是lua中,没有线程对应的锁存在,组装带有异常的失败信息,结束
            if (opStatus == null) {
                IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
                        + id + " thread-id: " + threadId);
                result.tryFailure(cause);
                return;
            }
      		// 正常的解锁流程,在这里取消自动续期,返回成功结果
            cancelExpirationRenewal(threadId);
            result.trySuccess(null);
        });

        return result;
    }

解锁内部实现

protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return commandExecutor.evalWriteAsync(getName(), 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.<Object>asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));

    }

我调试时,这段lua的参数是

KEYS:

  • MY_LOCK
  • redisson_lock__channel:{MY_LOCK}

ARGV:

  • 0
  • 30000
  • 81c39aea-b41f-4505-ae92-0f567fdc8651:1

解释下这段lua做了什么事情:

如果MY_LOCK的线程field不存在,返回null,结束

如果MY_LOCK的线程field存在,给value 减一,也就是重入锁退出一次。如果value还大于0,重新设置过期时间,返回0; 否则删除MY_LOCK, 往redisson_lock__channel:{MY_LOCK}通道发布一条消息0,返回1.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值