Redisson分佈式鎖的源碼分析

Redisson分佈式鎖的源碼分析

Redisson 分佈式鎖實現思路

鎖標識:Hash 數據結構,key 爲鎖的名字,filed 當前競爭鎖成功線程的唯一標識,value 重入次數

隊列:所有競爭鎖失敗的線程,會訂閱當前鎖的解鎖事件,利用 Semaphore 實現線程的掛起和喚醒

源碼分析

基於redisson3.11.5版本

加鎖流程圖

2019041502

解鎖核心源碼:tryLockInnerAsync
    <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        internalLockLeaseTime = unit.toMillis(leaseTime);

        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
                   //如果鎖不存在,則執行 hset 命令(hset key UUID+threadId 1),然後通過 pexpire 命令設置鎖的過期時間(即鎖的租約時間)
                  // 返回空值 nil ,表示獲取鎖成功
                  "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; " +
                  //如果鎖存在,且value匹配,說明是當前線程持有的鎖,則執行 hincrby 命令,重入次數加1,並且設置失效時間,返回空
                  "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; " +
                  //如果key已經存在,但是value不匹配,說明鎖已經被其他線程持有,通過 pttl 命令獲取鎖的剩餘存活時間並返回,至此獲取鎖失敗
                  "return redis.call('pttl', KEYS[1]);",
                    // 下面三個參數分別爲 KEYS[1], ARGV[1], ARGV[2]
                    // 即鎖的name,鎖釋放時間,當前線程唯一標識
                    Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
    }

參數說明:

  • KEYS[1]:Collections.singletonList(getName()),表示分佈式鎖的key;
  • ARGV[1]:internalLockLeaseTime,即鎖的租約時間(持有鎖的有效時間),默認30s;
  • ARGV[2]:getLockName(threadId),是獲取鎖時set的唯一值 value,即UUID+threadId。
解鎖流程圖

2019041503

解鎖核心源碼:unlockInnerAsync
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; " +
                //鎖存在,且值匹配,將增量(重入次數)-1,如果重入次數大於0,更新鎖的過期時間,不能釋放,返回 0 
                "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
                "if (counter > 0) then " +
                    "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                    "return 0; " +
                "else " +
                    //重入次數減1後的值如果爲0,刪除key,釋放鎖,返回 1 ;
                    "redis.call('del', KEYS[1]); " +
                    //發佈鎖釋放消息
                    "redis.call('publish', KEYS[2], ARGV[1]); " +
                    "return 1; "+
                "end; " +
                "return nil;",
                //這5個參數分別對應KEYS[1],KEYS[2],ARGV[1],ARGV[2]和ARGV[3]
                Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));

    }

參數說明:

  • KEYS[1]:getName(),表示分佈式鎖的key;
  • KEYS[2]:getChannelName(),redis消息的ChannelName,一個分佈式鎖對應唯一的一個 channelName;
  • ARGV[1]:LockPubSub.UNLOCK_MESSAGE,redis消息體,這裏只需要一個字節的標記就可以,主要標記redis的key已經解鎖,再結合redis的Subscribe,能喚醒其他訂閱解鎖消息的客戶端線程申請鎖;
  • ARGV[2]:internalLockLeaseTime,即鎖的租約時間(持有鎖的有效時間),默認30s;
  • ARGV[3]:getLockName(threadId),是獲取鎖時set的唯一值 value,即UUID+threadId。

總結

通過 Redisson 實現分佈式可重入鎖,比純自己通過set key value px milliseconds nx +lua 實現的效果更好些,雖然基本原理都一樣,因爲通過分析源碼可知,RedissonLock是可重入的,並且考慮了失敗重試,可以設置鎖的最大等待時間, 在實現上也做了一些優化,減少了無效的鎖申請,提升了資源的利用率。

需要特別注意的是,RedissonLock 同樣沒有解決redis節點掛掉的時候,存在丟失鎖的風險的問題。而現實情況是有一些場景無法容忍的,所以 Redisson 提供了實現了redlock算法的 RedissonRedLock,RedissonRedLock 真正解決了單點失敗的問題,代價是需要額外的爲 RedissonRedLock 搭建Redis環境。

所以,如果業務場景可以容忍這種小概率的錯誤,則推薦使用 RedissonLock, 如果無法容忍,則推薦使用 RedissonRedLock。

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