redisson之分散式鎖實現原理(三)

童話述說我的結局發表於2022-06-16

官網:https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95

一、什麼是分散式鎖

1.1、什麼是分散式鎖

分散式鎖,即分散式系統中的鎖。在單體應用中我們通過鎖解決的是控制共享資源訪問的問題,而分散式鎖,就是解決了分散式系統中控制共享資源訪問的問題。與單體應用不同的是,分散式系統中競爭共享資源的最小粒度從執行緒升級成了程式。

1.2、分散式鎖應該具備哪些條件

  • 在分散式系統環境下,一個方法在同一時間只能被一個機器的一個執行緒執行
  • 高可用的獲取鎖與釋放鎖
  • 高效能的獲取鎖與釋放鎖
  • 具備可重入特性(可理解為重新進入,由多於一個任務併發使用,而不必擔心資料錯誤)
  • 具備鎖失效機制,即自動解鎖,防止死鎖
  • 具備非阻塞鎖特性,即沒有獲取到鎖將直接返回獲取鎖失敗

1.3、分散式鎖的實現方式

  • 基於資料庫實現分散式鎖
  • 基於Zookeeper實現分散式鎖
  • 基於reids實現分散式鎖

二、基於資料庫的分散式鎖

基於資料庫的鎖實現也有兩種方式,一是基於資料庫表的增刪,另一種是基於資料庫排他鎖。

2.1、基於資料庫表的增刪

基於資料庫表增刪是最簡單的方式,首先建立一張鎖的表主要包含下列欄位:類的全路徑名+方法名,時間戳等欄位。

具體的使用方式:當需要鎖住某個方法時,往該表中插入一條相關的記錄。類的全路徑名+方法名是有唯一性約束的,如果有多個請求同時提交到資料庫的話,資料庫會保證只有一個操作可以成功,那麼我們就認為操作成功的那個執行緒獲得了該方法的鎖,可以執行方法體內容。執行完畢之後,需要delete該記錄。

(這裡只是簡單介紹一下,對於上述方案可以進行優化,如:應用主從資料庫,資料之間雙向同步;一旦掛掉快速切換到備庫上;做一個定時任務,每隔一定時間把資料庫中的超時資料清理一遍;使用while迴圈,直到insert成功再返回成功;記錄當前獲得鎖的機器的主機資訊和執行緒資訊,下次再獲取鎖的時候先查詢資料庫,如果當前機器的主機資訊和執行緒資訊在資料庫可以查到的話,直接把鎖分配給他就可以了,實現可重入鎖)

2.2、基於資料庫排他鎖

基於MySql的InnoDB引擎,可以使用以下方法來實現加鎖操作

public void lock(){
    connection.setAutoCommit(false)
    int count = 0;
    while(count < 4){
        try{
            select * from lock where lock_name=xxx for update;
            if(結果不為空){
                //代表獲取到鎖
                return;
            }
        }catch(Exception e){
 
        }
        //為空或者拋異常的話都表示沒有獲取到鎖
        sleep(1000);
        count++;
    }
    throw new LockException();
}

在查詢語句後面增加for update,資料庫會在查詢過程中給資料庫表增加排他鎖。獲得排它鎖的執行緒即可獲得分散式鎖,當獲得鎖之後,可以執行方法的業務邏輯,執行完方法之後,釋放鎖connection.commit()。當某條記錄被加上排他鎖之後,其他執行緒無法獲取排他鎖並被阻塞。

2.3、基於資料庫鎖的優缺點

上面兩種方式都是依賴資料庫表,一種是通過表中的記錄判斷當前是否有鎖存在,另外一種是通過資料庫的排他鎖來實現分散式鎖。

  • 優點是直接藉助資料庫,簡單容易理解。
  • 缺點是運算元據庫需要一定的開銷,效能問題需要考慮。

三、基於Zookeeper的分散式鎖

       基於zookeeper臨時有序節點可以實現的分散式鎖。每個客戶端對某個方法加鎖時,在zookeeper上的與該方法對應的指定節點的目錄下,生成一個唯一的瞬時有序節點。 判斷是否獲取鎖的方式很簡單,只需要判斷有序節點中序號最小的一個。 當釋放鎖的時候,只需將這個瞬時節點刪除即可。同時,其可以避免服務當機導致的鎖無法釋放,而產生的死鎖問題。 (第三方庫有 Curator,Curator提供的InterProcessMutex是分散式鎖的實現)

Zookeeper實現的分散式鎖存在兩個個缺點:

(1)效能上可能並沒有快取服務那麼高,因為每次在建立鎖和釋放鎖的過程中,都要動態建立、銷燬瞬時節點來實現鎖功能。ZK中建立和刪除節點只能通過Leader伺服器來執行,然後將資料同步到所有的Follower機器上。

(2)zookeeper的併發安全問題:因為可能存在網路抖動,客戶端和ZK叢集的session連線斷了,zk叢集以為客戶端掛了,就會刪除臨時節點,這時候其他客戶端就可以獲取到分散式鎖了。

 四、基於redis的分散式鎖

redis命令說明:

(1)setnx命令:set if not exists,當且僅當 key 不存在時,將 key 的值設為 value。若給定的 key 已經存在,則 SETNX 不做任何動作。

返回1,說明該程式獲得鎖,將 key 的值設為 value
返回0,說明其他程式已經獲得了鎖,程式不能進入臨界區。
命令格式:setnx lock.key lock.value

(2)get命令:獲取key的值,如果存在,則返回;如果不存在,則返回nil

命令格式:get lock.key

(3)getset命令:該方法是原子的,對key設定newValue這個值,並且返回key原來的舊值。

命令格式:getset lock.key newValue

(4)del命令:刪除redis中指定的key

命令格式:del lock.key

方案一:基於set命令的分散式鎖

1、加鎖:使用setnx進行加鎖,當該指令返回1時,說明成功獲得鎖

2、解鎖:當得到鎖的執行緒執行完任務之後,使用del命令釋放鎖,以便其他執行緒可以繼續執行setnx命令來獲得鎖

1)存在的問題:假設執行緒獲取了鎖之後,在執行任務的過程中掛掉,來不及顯示地執行del命令釋放鎖,那麼競爭該鎖的執行緒都會執行不了,產生死鎖的情況。

(2)解決方案:設定鎖超時時間

3、設定鎖超時時間:setnx 的 key 必須設定一個超時時間,以保證即使沒有被顯式釋放,這把鎖也要在一定時間後自動釋放。可以使用expire命令設定鎖超時時間

1)存在問題:

setnx 和 expire 不是原子性的操作,假設某個執行緒執行setnx 命令,成功獲得了鎖,但是還沒來得及執行expire 命令,伺服器就掛掉了,這樣一來
,這把鎖就沒有設定過期時間了,變成了死鎖,別的執行緒再也沒有辦法獲得鎖了。 (
2)解決方案:redis的set命令支援在獲取鎖的同時設定key的過期時間

4、使用set命令加鎖並設定鎖過期時間:

命令格式:set <lock.key> <lock.value> nx ex <expireTime>

詳情參考redis使用文件:http://doc.redisfans.com/string/set.html

1)存在問題:

① 假如執行緒A成功得到了鎖,並且設定的超時時間是 30 秒。如果某些原因導致執行緒 A 執行的很慢,過了 30 秒都沒執行完,這時候鎖過期自動釋放,
執行緒 B 得到了鎖。 ② 隨後,執行緒A執行完任務,接著執行del指令來釋放鎖。但這時候執行緒 B 還沒執行完,執行緒A實際上刪除的是執行緒B加的鎖。 (
2)解決方案: 可以在 del 釋放鎖之前做一個判斷,驗證當前的鎖是不是自己加的鎖。在加鎖的時候把當前的執行緒 ID 當做value,並在刪除之前驗證 key 對應的
value 是不是自己執行緒的 ID。但是,這樣做其實隱含了一個新的問題,get操作、判斷和釋放鎖是兩個獨立操作,不是原子性。對於非原子性的問題,
我們可以使用Lua指令碼來確保操作的原子性

5、鎖續期:(這種機制類似於redisson的看門狗機制,文章後面會詳細說明)

雖然步驟4避免了執行緒A誤刪掉key的情況,但是同一時間有 A,B 兩個執行緒在訪問程式碼塊,仍然是不完美的。怎麼辦呢?我們可以讓獲得鎖的執行緒開啟一個守護執行緒,用來給快要過期的鎖“續期”。

① 假設執行緒A執行了29 秒後還沒執行完,這時候守護執行緒會執行 expire 指令,為這把鎖續期 20 秒。守護執行緒從第 29 秒開始執行,每 20 秒執行
一次。 ② 情況一:當執行緒A執行完任務,會顯式關掉守護執行緒。 ③ 情況二:如果伺服器忽然斷電,由於執行緒 A 和守護執行緒在同一個程式,守護執行緒也會停下。這把鎖到了超時的時候,沒人給它續命,也就自動釋放了。

 五、Redisson 是什麼

       Redisson 是架設在 Redis 基礎上的一個 Java駐記憶體資料網格框架, 充分利用 Redis 鍵值資料庫提供的一系列優勢, 基於 Java 使用工具包中常用介面, 為使用者提供了 一系列具有分散式特性的常用工具類;使得原本作為協調單機多執行緒併發程式的工具包 獲得了協調分散式多機多執行緒併發系統的能力, 大大降低了設計和研發大規模分散式系統的難度,同時結合各富特色的分散式服務, 更進一步 簡化了分散式環境中程式相互之間的協作。

5.1、Redisson 重入鎖

由於 Redisson 太過於複雜, 設計的 API 呼叫大多用 Netty 相關, 所以這裡只對 如何加鎖、如何實現重入鎖進行分析以及如何鎖續時進行分析

5.2、建立鎖

下面這個簡單的程式, 就是使用 Redisson 建立了一個非公平的可重入鎖,lock() 方法加鎖成功 預設過期時間 30 秒, 並且支援 "看門狗" 續時功能。

匯入pom

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.16.0</version>
        </dependency>

把上節課的程式碼改下

@RestController
@RequestMapping("/redisson")
public class RedissonController {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    RedissonClient redissonClient;

    @GetMapping("/save")
    public String save(){
        stringRedisTemplate.opsForValue().set("key","redisson");
        return "save ok";
    }

    @GetMapping("/get")
    public String get(){

        RLock lock=redissonClient.getLock ( "myLock" );
        String str=null;
            if (lock.tryLock ()){
                System.out.println ("拿到了鎖");
                str=stringRedisTemplate.opsForValue().get("key");

            }else{
                System.out.println ("沒有拿到鎖");
            }
            lock.unlock ();

        return str;
    }

}

然後自己用JMeter工具自己測試下就知道結果了

六、Redisson分散式鎖的實現原理

       通過redisson,非常簡單就可以實現我們所需要的功能,當然這只是redisson的冰山一角,redisson最強大的地方就是提供了分散式特性的常用工具類。使得原本作為協調單機多執行緒併發程式的併發程式的工具包獲得了協調分散式多級多執行緒併發系統的能力,降低了程式設計師在分散式環境下解決分散式問題的難度,下面分析一下RedissonLock的實現原理;

 

 

 

    public boolean tryLock() {
        return (Boolean)this.get(this.tryLockAsync());
    }
    @Override
    public RFuture<Boolean> tryLockAsync(long threadId) {
        return tryAcquireOnceAsync(-1, -1, null, threadId);
    }

RedissonLock.tryAcquireOnceAsync

一直點跟到最後發現進入了下面這個方法,RedissonLock不同的加鎖方法,流程會有所差別:
tryLock()不帶引數最終呼叫的是下面程式碼,從過上面tryLockAsync方法可知,我們預設不傳遞引數時,程式會預設配置引數,其中傳過來的引數leaseTime為-1,unint是null,internalLockLeaseTime預設設定是30S

    private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
        RFuture ttlRemainingFuture;
//leaseTime就是租約時間,就是redis key的過期時間。
if (leaseTime != -1L) {//如果設定過期時間 ttlRemainingFuture = this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN); } else {
//否則,在獲取鎖之後,需要加上定時任務,給鎖設定一個內部過期時間,並不斷重新整理這個時間直到釋放鎖 ttlRemainingFuture
= this.tryLockInnerAsync(waitTime, this.internalLockLeaseTime, TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN); }

  //當tryLockInnerAsync執行結束後,觸發下面回撥
  ttlRemainingFuture.onComplete((ttlRemaining, e) -


        ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
            if (e == null) {
//lock acquired
//獲取鎖成功後,呼叫定時任務延遲鎖時間
if (ttlRemaining) {
//表示設定過期時間,更新internalLockLeaseTime
if (leaseTime != -1L) { this.internalLockLeaseTime = unit.toMillis(leaseTime); } else {
// 設定一個定時任務
this.scheduleExpirationRenewal(threadId); } } } }); return ttlRemainingFuture; }

tryLockInnerAsync加鎖實現

此段指令碼為一段lua指令碼:
KEY[1]: 為你加鎖的lock值
ARGV[2]: 為執行緒id
ARGV[1]: 為設定的過期時間

第一個if:
判斷是否存在設定lock的key是否存在,不存在則利用redis的hash結構設定一個hash,值為1,並設定過期時間,後續返回鎖。
第二個if:
判斷是否存在設定lock的key是否存在,存在此執行緒的hash,則為這個鎖的重入次數加1(將hash值+1),並重新設定過期時間,後續返回鎖。
最後返回:
這個最後返回不是說最後結果返回,是代表以上兩個if都沒有進入,則代表處於競爭鎖的情況,後續返回競爭鎖的過期時間

<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
return this.evalWriteAsync(this.getRawName(), LongCodec.INSTANCE, command,
// 這裡面的 KEYS[1] 就是我們在最開始獲取的Redis的那把鎖,看那個屬性或者說是鎖是否存在
"if (redis.call('exists', KEYS[1]) == 0) " +
"then " +
// 如果不存在,走下面邏輯,給當前執行緒設定值加1,也就是1
"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 " +
// 給當前執行緒的數值加1
"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(this.getRawName()), new Object[]{unit.toMillis(leaseTime), this.getLockName(threadId)});
}

通過這種eval表示式,lua指令碼保證原子性
如果不存在鎖:
等價於:

命令 備註
EXISTS lockkey 判斷鎖是否存在
HSET lockkey uuid:threadId 1 設定hash field和value值
PEXPIRE lockkey internalLockLeaseTime 設定lockkey的過期時間

 

 

 

 

 

如果存在鎖:
等價於:

命令 備註
HEXISTS lockkey uuid:threadId 判斷當前執行緒是否已經獲取到鎖
HSET lockkey uuid:threadId 1 設定hash field和value值
HINCRBY lockkey uuid:threadId 1 給對應的field的值加1(相當於可重入)
PEXPIRE lockkey internalLockLeaseTime 重置過期時間

 

 

 

 

 

 

普通的不帶參加鎖邏輯就結束了。

tryLock帶引數就相對複雜一些,加入了執行緒自旋相關的邏輯處理:

tryLock具有返回值,true或者false,表示是否成功獲取鎖。

   @Override
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
long time = unit.toMillis(waitTime);
long current = System.currentTimeMillis();
long threadId = Thread.currentThread().getId();
//走tryAcquireAsync的邏輯
Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return true;
}
// 獲取鎖失敗後,中途tryLock會一直判斷中間操作耗時是否已經消耗鎖的過期時間,如果消耗完則返回false
time -= System.currentTimeMillis() - current;
if (time <= 0) {
//如果已經超時,則直接失敗
acquireFailed(waitTime, unit, threadId);
return false;
}

current = System.currentTimeMillis();
// 訂閱鎖釋放事件
// 如果當前執行緒通過 Redis 的 channel 訂閱鎖的釋放事件獲取得知已經被釋放,則會發訊息通知待等待的執行緒進行競爭.

RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
// 將訂閱阻塞,阻塞時間設定為我們呼叫tryLock設定的最大等待時間,超過時間則返回false
if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
if (!subscribeFuture.cancel(false)) {
subscribeFuture.onComplete((res, e) -> {
if (e == null) {
unsubscribe(subscribeFuture, threadId);
}
});
}
acquireFailed(waitTime, unit, threadId);
return false;
}
//收到訂閱的訊息後走的邏輯
try {
time -= System.currentTimeMillis() - current;
//判斷時間是否超時
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
// 迴圈獲取鎖,但由於上面有最大等待時間限制,基本會在上面返回false
while (true) {
long currentTime = System.currentTimeMillis();
ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return true;
}

time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}

                 // 通過訊號量(共享鎖)阻塞,等待解鎖訊息. (減少申請鎖呼叫的頻率)
                 // 如果剩餘時間(ttl)小於wait time ,就在 ttl 時間內,從Entry的訊號量獲取一個許可(除非被中斷或者一直沒有可用的許可)。
                  // 否則就在wait time 時間範圍內等待可以通過訊號量

                // waiting for message
currentTime = System.currentTimeMillis();
if (ttl >= 0 && ttl < time) {
subscribeFuture.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
subscribeFuture.getNow().getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
}

time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
}
} finally {
unsubscribe(subscribeFuture, threadId);
}
// return get(tryLockAsync(waitTime, leaseTime, unit));
}

到這裡帶參加鎖邏輯就結束了。接下來聊下釋放鎖過程

unlock() 釋放鎖

可重入鎖 RedissonLock 釋放鎖的原始碼還是比較簡單的,我們可以分為兩步:第一步是執行釋放鎖的lua指令碼,第二步就是停止 watchdog 的執行。

RedissonBaseLock.unlockAsync

@Override
public RFuture<Void> unlockAsync(long threadId) {
    RPromise<Void> result = new RedissonPromise<Void>();
    RFuture<Boolean> future = unlockInnerAsync(threadId);

    future.onComplete((opStatus, e) -> {
        // 停止 watchdog 的定時續過期時間,其實就是將對應的 ExpirationEntry 從 EXPIRATION_RENEWAL_MAP 中移除,當 watchdog 執行時發現當前客戶端當前執行緒沒有 ExpirationEntry 了,那麼就會停止執行了。
        cancelExpirationRenewal(threadId);

        if (e != null) {
            result.tryFailure(e);
            return;
        }
        
        // 如果 lua指令碼返回的是null,證明當前執行緒之前並沒有成功獲取鎖,執行tryFailure方法
        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;
        }
        // 成功釋放鎖,執行 trySuccess 方法
        result.trySuccess(null);
    });

    return result;
}

釋放鎖 lua 指令碼

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

Redis提供了一組命令可以讓開發者實現“釋出/訂閱”模式(publish/subscribe) . 該模式同樣可以實現程式間的訊息傳遞,它的實現原理是:

釋出/訂閱模式包含兩種角色,分別是釋出者和訂閱者。訂閱者可以訂閱一個或多個頻道,而釋出者可以向指定的頻道傳送訊息,所有訂閱此頻道的訂閱者都會收到該訊息

釋出者釋出訊息的命令是PUBLISH, 用法是

PUBLISH channel message

比如向channel.1發一條訊息:hello

PUBLISH channel.1 “hello”

這樣就實現了訊息的傳送,該命令的返回值表示接收到這條訊息的訂閱者數量。因為在執行這條命令的時候還沒有訂閱者訂閱該頻道,所以返回為0. 另外值得注意的是訊息傳送出去不會持久化,如果傳送之前沒有訂閱者,那麼後續再有訂閱者訂閱該頻道,之前的訊息就收不到了

訂閱者訂閱訊息的命令是:

SUBSCRIBE channel [channel …]

該命令同時可以訂閱多個頻道,比如訂閱channel.1的頻道:SUBSCRIBE channel.1,執行SUBSCRIBE命令後客戶端會進入訂閱狀態。

到這裡釋放鎖的過程也說完了,但還有一個問題,那就是鎖過期了怎麼辦;

如何解決鎖過期問題

       一般來說,我們去獲得分散式鎖時,為了避免死鎖的情況,我們會對鎖設定一個超時時間,但是有一種情況是,如果在指定時間內當前執行緒沒有執行完,由於鎖超時導致鎖被釋放,那麼其他執行緒就會拿到這把鎖,從而導致一些故障。

為了避免這種情況,Redisson引入了一個Watch Dog機制,這個機制是針對分散式鎖來實現鎖的自動續約,簡單來說,如果當前獲得鎖的執行緒沒有執行完,那麼Redisson會自動給Redis中目標key延長超時時間。預設情況下,看門狗的續期時間是30s,也可以通過修改Config.lockWatchdogTimeout來另行指定。

@Override
public boolean tryLock(long waitTime, TimeUnit unit) throws InterruptedException {
    return tryLock(waitTime, -1, unit);  //leaseTime=-1
}

實際上,當我們通過tryLock方法沒有傳遞超時時間時,預設會設定一個30s的超時時間,避免出現死鎖的問題。

private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    RFuture<Long> ttlRemainingFuture;
    if (leaseTime != -1) { 
        ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    } else { //當leaseTime為-1時,leaseTime=internalLockLeaseTime,預設是30s,表示當前鎖的過期時間。
        
        //this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
        ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
                                               TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    }
    ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
        if (e != null) { //說明出現異常,直接返回
            return;
        }
        // lock acquired
        if (ttlRemaining == null) { //表示第一次設定鎖鍵
            if (leaseTime != -1) { //表示設定過超時時間,更新internalLockLeaseTime,並返回
                internalLockLeaseTime = unit.toMillis(leaseTime);
            } else { //leaseTime=-1,啟動Watch Dog
                scheduleExpirationRenewal(threadId);
            }
        }
    });
    return ttlRemainingFuture;
}

由於預設設定了一個30s的過期時間,為了防止過期之後當前執行緒還未執行完,所以通過定時任務對過期時間進行續約。

  • 首先,會先判斷在expirationRenewalMap中是否存在了entryName,這是個map結構,主要還是判斷在這個服務例項中的加鎖客戶端的鎖key是否存在,
  • 如果已經存在了,就直接返回;主要是考慮到RedissonLock是可重入鎖。
protected void scheduleExpirationRenewal(long threadId) {
    ExpirationEntry entry = new ExpirationEntry();
    ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
    if (oldEntry != null) {
        oldEntry.addThreadId(threadId);
    } else {// 第一次加鎖的時候會呼叫,內部會啟動WatchDog
        entry.addThreadId(threadId);
        renewExpiration();
    
    }
}

定義一個定時任務,該任務中呼叫renewExpirationAsync方法進行續約。

private void renewExpiration() {
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (ee == null) {
        return;
    }
    //用到了時間輪機制
    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;
            }
            // renewExpirationAsync續約租期
            RFuture<Boolean> future = renewExpirationAsync(threadId);
            future.onComplete((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();
                }
            });
        }
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);//每次間隔租期的1/3時間執行
 
    ee.setTimeout(task);
}

執行Lua指令碼,對指定的key進行續約。

protected RFuture<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));
}

到這裡所有流程就全部講完了

 

相關文章