通過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.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章