Redisson分佈式鎖學習總結:公平鎖 RedissonFairLock#unLock 釋放鎖源碼分析

原文鏈接:Redisson分佈式鎖學習總結:公平鎖 RedissonFairLock#unLock 釋放鎖源碼分析

一、RedissonFairLock#unlock 源碼分析

上一篇講到,RedissonFairLock 它主要是基於 RedissonLock 做的擴展,主要擴展在於加鎖和釋放鎖的地方。所以我們要研究 RedissonFairLock 釋放鎖的邏輯,只需要關注它重寫的 RedissonLock#unlockAsync 方法即可。至於如何計算 slot 來執行 lua 腳本,釋放後需停止 wathchdog 的運行,和 RedissonLock 保持一致,我們只要研究了 RedissonLock 釋放鎖的邏輯後,這裏就不再需要關注了。那我們下面直接上lua腳本的分析。

1、RedissonFairLock 之 lua 腳本釋放鎖

RedissonFairLock#unlockInnerAsync:


@Override
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
    return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            // remove stale threads
            "while true do "
            + "local firstThreadId2 = redis.call('lindex', KEYS[2], 0);"
            + "if firstThreadId2 == false then "
                + "break;"
            + "end; "
            + "local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2));"
            + "if timeout <= tonumber(ARGV[4]) then "
                + "redis.call('zrem', KEYS[3], firstThreadId2); "
                + "redis.call('lpop', KEYS[2]); "
            + "else "
                + "break;"
            + "end; "
          + "end;"
            
          + "if (redis.call('exists', KEYS[1]) == 0) then " + 
                "local nextThreadId = redis.call('lindex', KEYS[2], 0); " + 
                "if nextThreadId ~= false then " +
                    "redis.call('publish', KEYS[4] .. ':' .. nextThreadId, ARGV[1]); " +
                "end; " +
                "return 1; " +
            "end;" +
            "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; " +
            "end; " +
                
            "redis.call('del', KEYS[1]); " +
            "local nextThreadId = redis.call('lindex', KEYS[2], 0); " + 
            "if nextThreadId ~= false then " +
                "redis.call('publish', KEYS[4] .. ':' .. nextThreadId, ARGV[1]); " +
            "end; " +
            "return 1; ",
            Arrays.asList(getName(), threadsQueueName, timeoutSetName, getChannelName()),
            LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId), System.currentTimeMillis());
}

因爲基於 RedissonLock 加入了公平獲取鎖的機制,所以lua腳本還是挺長的,下面我們一步一步分析。

1.1、KEYS

Arrays.asList(getName(), threadsQueueName, timeoutSetName, getChannelName()):

  • getName(): 鎖key
  • threadsQueueName:prefixName("redisson_lock_queue", name),用於鎖排隊
  • timeoutSetName:prefixName("redisson_lock_timeout", name),用於隊列中每個客戶端的等待超時時間
  • getChannelName:prefixName("redisson_lock__channel", getName()),當前鎖的訂閱channel

KEYS:["myLock","redisson_lock_queue:{myLock}","redisson_lock_timeout:{myLock}","redisson_lock__channel:{myLock}"]

1.2、ARGVS

LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId), System.currentTimeMillis():

  • LockPubSub.UNLOCK_MESSAGE:0L
  • internalLockLeaseTime:30_000毫秒,watchdog的超時時間
  • getLockName(threadId):return id + ":" + threadId,客戶端ID(UUID):線程ID(threadId)
  • System.currentTimeMillis():當前時間時間戳

ARGVS:[0L,3w毫秒,"UUID:threadId",當前時間時間戳]

1.3、lua 腳本分析

1、分支一:清理過期的等待線程

場景:

和獲取鎖的第一步一樣,開個死循環清理過期的等待線程,主要避免下面場景,避免無效客戶端佔用等待隊列資源

  • 獲取鎖失敗,然後進入等待隊列,但是網絡出現問題,那麼後續很有可能就不能繼續正常獲取鎖了。
  • 獲取鎖失敗,然後進入等待隊列,但是之後客戶端所在服務器宕機了。

lua腳本:

"while true do " +
    "local firstThreadId2 = redis.call('lindex', KEYS[2], 0);" +
    "if firstThreadId2 == false then " +
        "break;" +
    "end;" +

    "local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2));" +
    "if timeout <= tonumber(ARGV[4]) then " +
        // remove the item from the queue and timeout set
        // NOTE we do not alter any other timeout
        "redis.call('zrem', KEYS[3], firstThreadId2);" +
        "redis.call('lpop', KEYS[2]);" +
    "else " +
        "break;" +
    "end;" +
"end;" +

分析:

  1. 開啓死循環

  2. 利用 lindex 命令判斷等待隊列中第一個元素是否存在,如果存在,直接跳出循環

    lidex redisson_lock_queue:{myLock} 0
    
  3. 如果等待隊列中第一個元素不爲空(例如返回了LockName,即客戶端UUID拼接線程ID),利用 zscore 在 超時記錄集合(sorted set) 中獲取對應的超時時間

    zscore redisson_lock_timeout:{myLock} UUID:threadId
    
  4. 如果超時時間已經小於當前時間,那麼首先從超時集合中移除該節點,接着也在等待隊列中彈出第一個節點

    zrem redisson_lock_timeout:{myLock} UUID:threadId
    lpop redisson_lock_queue:{myLock}
    
  5. 如果等待隊列中的第一個元素還未超時,直接退出死循環

2、分支二:鎖已經被釋放,通知等待隊列中第一個線程

場景:

  • 成功獲取鎖線程重複調用釋放鎖的方法,第二次釋放時,鎖已不存在,就去通知等待隊列中的第一個元素
  • 又或者一個極端場景:當前線程未能成功獲取鎖,但是調用了釋放鎖的方法,並且剛好此時鎖被釋放

lua腳本:

"if (redis.call('exists', KEYS[1]) == 0) then " + 
    "local nextThreadId = redis.call('lindex', KEYS[2], 0); " + 
    "if nextThreadId ~= false then " +
        "redis.call('publish', KEYS[4] .. ':' .. nextThreadId, ARGV[1]); " +
    "end; " +
    "return 1; " +
"end;"

分析:

  1. 利用 exists 命令判斷鎖是否存在

    exists myLock
    
  2. 如果鎖不存在,利用 lidex 獲取等待隊列中的第一個元素

    lindex redisson_lock_queue:{myLock} 0
    
  3. 如果等待列表中第一個元素不爲空,即還存在等待線程,往等待線程的訂閱channel發送消息,通知其可以嘗試獲取鎖了

    publish redisson_lock__channel:{myLock}:UUID:threadId 0
    

    關於這個訂閱通道,之前介紹 RedissonLock 死循環等待鎖的時候沒有詳細介紹,這個可以補充一下。
    RedissonLock 所有等待線程都是訂閱鎖的同一個channel:redisson_lock__channel:{myLock},當有線程釋放鎖的時候,會往這個通道發送消息,此時所有等待現成都可以訂閱消費這條消息,從而從等待狀態中釋放出來,重新嘗試獲取鎖。

    而 RedissonFairLock 不太一樣,因爲它要支持公平獲取鎖,即先到先得。所以每個等待線程訂閱的都是不同的channel:redisson_lock__channel:{myLock}:UUID:threadId。當某個線程釋放鎖的時候,只會往等待隊列中第一個線程對應訂閱的channel發送消息。

  4. 最後,返回1

3、分支三:加鎖記錄中的線程不是當前線程

場景:

  • 當前線程未能成功獲取鎖,但是調用了釋放鎖的方法

lua腳本:

"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
    "return nil;" +
"end; "

分析:

  1. 利用 hexists 命令判斷加鎖記錄集合中,是否存在當前客戶端當前線程
    hexists myLock UUID:threadId
    
  2. 加鎖記錄不存在當前線程,返回nil
    如果返回null,會打印相關日誌,並調用 tryFailure 方法。

4、分支四:當前線程擁有鎖,並且獲取鎖次數大於1

場景:

  • 當前線程擁有鎖,並且重入次數大於1

lua腳本:

"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
"if (counter > 0) then " +
    "redis.call('pexpire', KEYS[1], ARGV[2]); " +
    "return 0; " +
"end; "

分析:

  1. 利用 hincrby 扣減當前線程的加鎖次數

    hincrby myLock UUID:threadId -1
    
  2. 如果扣減後次數還是大於0,證明是重複獲取鎖,所以此時只需要重新刷新鎖的過期時間,然後返回0

    expire myLock 30_000
    

5、分支五:當前線程成功釋放鎖

場景:

  • 如果前面的四個分支都不符合,證明當前線程持有鎖,並且只加鎖一次
  • 這裏只需要刪除鎖key,然後通知等待隊列中的第一個線程即可

lua腳本:

"redis.call('del', KEYS[1]); " +
"local nextThreadId = redis.call('lindex', KEYS[2], 0); " + 
"if nextThreadId ~= false then " +
    "redis.call('publish', KEYS[4] .. ':' .. nextThreadId, ARGV[1]); " +
"end; " +
"return 1; "

分析:

  1. 利用 del 命令刪除鎖對應 redis key

    del myLock
    
  2. 往等待線程的訂閱channel發送消息,通知其可以嘗試獲取鎖了

    • 利用 lindex 獲取等待隊列中的第一個線程
    lindex redisson_lock_queue:{myLock} 0
    
    • 利用訂閱channel給等待線程發送通知
    publish redisson_lock__channel:{myLock}:UUID:threadId 0
    

1.4 後續

接着調用釋放鎖後續的操作:例如停止 watchdog 運行、執行 tryFailure 或 trySuccess 等和 RedissonClient 保持一致,這裏就不再詳細介紹了~

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