日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

一、 分布式鎖簡介

分布式鎖是一種常見的協調分布式系統的機制,在分布式環境下保證數據的一致性和可用性。分布式鎖的實現有很多種方式,其中較為常見的方式是利用redis實現分布式鎖。

在使用 Redis 實現分布式鎖時,我們通常使用 SET key value [EX seconds] [NX] 命令來給某個 key 設置一個具有過期時間的值作為鎖。其中 EX 參數表示設置過期時間,當 Redis 客戶端連接斷開或者達到過期時間時,鎖會自動失效。但是,在一些特殊情況下,由于網絡波動等原因,我們可能無法及時續期更新鎖的過期時間,這會導致鎖在沒有被顯式釋放的情況下過期,從而引發并發問題。

Redisson 的看門狗就是為了解決這個問題而設計的。它會在獲取鎖之后啟動一個后臺任務定期地對鎖進行“續期”,即更新鎖的過期時間。具體來說,每次啟動續期任務時,會通過 set(key, value, XX, PX, ttl) 命令更新鎖的過期時間,同時記錄該任務與鎖的對應關系。在鎖釋放或過期時,會取消相應的續期任務,從而保證鎖的有效性。

二、 Redisson分布式鎖的看門狗源碼分析

Redisson 的 RedissonLock 類主要通過下面五個方法實現Watchdog機制:

  • tryAcquireAsync
  • scheduleExpirationRenewal
  • renewExpiration
  • renewExpirationAsync
  • cancelExpirationRenewal

下面講解各個方法的源碼片段

1. tryAcquireAsync

private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
    // 如果設置了超時時間,調用 tryLockInnerAsync 方法嘗試加鎖并設置過期時間。
    if (leaseTime != -1) {
        return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    }

    // 否則僅獲取鎖,并返回剩余時間。
    // 計算剩余過期時間
    RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
            TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);

    // 當獲取剩余過期時間的異步操作完成后,判斷是否獲取到鎖,如果獲取到,開始定時任務自動續期
    ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
        if (e != null) {
            return;
        }
        // 如果返回值為 null 表示已經擁有鎖
        if (ttlRemaining == null) {
            // 開始定期檢查鎖是否過期,如果沒有過期則續期
            scheduleExpirationRenewal(threadId);
        }
    });
    return ttlRemainingFuture;
}

Redisson在lock方法執行時,會調用tryAcquireAsync方法獲取鎖,在獲取到鎖時,上面示例代碼種會調用scheduleExpirationRenewal(threadId)方法,開啟定時檢查是否過期和自動續期的定時任務,這里其實就是看門狗機制的創建點。

2. scheduleExpirationRenewal

private void scheduleExpirationRenewal(long threadId) {
    ExpirationEntry entry = new ExpirationEntry();

    // 在 EXPIRATION_RENEWAL_MAP 中添加一個新的鍵值對,如果該鍵名已存在,則將 threadId 添加到該鍵名的隊列中。
    ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
    if (oldEntry != null) {
        oldEntry.addThreadId(threadId);
    } else { // 如果該鍵名不存在,則添加一個新的鍵值對,并執行 renewExpiration() 方法續約該鎖的過期時間。
        entry.addThreadId(threadId);
        renewExpiration();
    }
}

當一個線程獲取到鎖時,會調用 scheduleExpirationRenewal() 方法向 EXPIRATION_RENEWAL_MAP (ConcurrentHashMap 對象)中添加一個新的鍵值對,如果該鍵名已存在,則將 threadId 添加到該鍵名的隊列中;否則,添加一個新的鍵值對,并調用 renewExpiration() 方法續約該鎖的過期時間。

3. renewExpiration

private void renewExpiration() {
    // 從 EXPIRATION_RENEWAL_MAP 中查找當前鎖實例,并獲取相應的 ExpirationEntry 對象。
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (ee == null) {
        return;
    }

    // 創建一個定時任務(Timeout),在線程持有該鎖時執行續期操作。
    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) {
                    renewExpiration();
                }
            });
        }
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); // 定時任務的間隔為鎖到期時間的三分之一

    // 將該定時任務存儲在 ExpirationEntry 實體中,方便后續處理。
    ee.setTimeout(task);
}

在該方法內部,首先通過 getEntryName() 獲取當前鎖實例的名稱,然后從 EXPIRATION_RENEWAL_MAP 中查找該名稱對應的 ExpirationEntry 對象。如果對象不存在,則直接返回即可;否則,創建一個新的定時任務,該任務會在 internalLockLeaseTime / 3 毫秒后執行,并嘗試異步更新鎖的過期時間。如果更新成功,則會再次調用 renewExpiration() 方法,以便持續延長鎖的過期時間。這個定時任務通俗的講就是所謂的看門狗。當然,這里更新過期時間的操作是通過調用 renewExpirationAsync() 實現的,它仍然是一個異步操作。

4. renewExpirationAsync

protected RFuture<Boolean> renewExpirationAsync(long threadId) {
    // 使用 evalWriteAsync 方法執行 EVAL 命令
    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            // EVAL 命令的腳本,根據給定的鍵和參數進行判斷和更新
            "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                // 如果指定的鍵和參數都存在,則續約該鍵的過期時間
                "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                // 并返回操作成功的標志
                "return 1; " +
            "end; " +
            // 如果指定的鍵和參數不匹配,則返回操作失敗的標志
            "return 0;",
        // EVAL 命令中用到的鍵
        Collections.<Object>singletonList(getName()), 
        // 續約的過期時間,在 Redisson 中為 internalLockLeaseTime
        internalLockLeaseTime, 
        // 獲取鎖的名稱,由線程 ID 和當前 Redisson 實例 ID 組成
        getLockName(threadId)
    );
}

此方法主要是用于在獲取分布式鎖的情況下,對鎖的過期時間進行續約的操作。其中,
RedisCommands.EVAL_BOOLEAN 代表執行 EVAL 命令后返回的數據類型為 Boolean 類型;getKey() 方法用于獲取鎖的名稱,該名稱由鎖的前綴和鎖的 ID 組成;getLockName(threadId) 方法用于獲取當前線程獲取鎖后的鎖名稱,之所以要使用當前 Redisson 實例 ID 與線程 ID 組合作為鎖名稱,是為了確保在多個 Redisson 實例下,所有線程都能夠正確地獲取到鎖名并正確地執行續約操作。

5.cancelExpirationRenewal

void cancelExpirationRenewal(Long threadId) {
    // 從 EXPIRATION_RENEWAL_MAP 中查找當前鎖實例,并獲取相應的 ExpirationEntry 對象。
    ExpirationEntry task = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (task == null) {
        return;
    }

    // 如果傳入了調用者線程 ID,則從任務中移除該線程 ID。
    if (threadId != null) {
        task.removeThreadId(threadId);
    }

    // 如果線程 ID 為空,或者任務已經沒有任何線程在持有它了,則取消任務并從 EXPIRATION_RENEWAL_MAP 中刪除該實體。
    if (threadId == null || task.hasNoThreads()) {
        Timeout timeout = task.getTimeout();
        if (timeout != null) {
            timeout.cancel(); // 取消定時任務
        }
        EXPIRATION_RENEWAL_MAP.remove(getEntryName()); // 從 EXPIRATION_RENEWAL_MAP 中刪除實體
    }
}

這個方法主要是用于取消延長鎖過期時間的定時任務。當一個線程在unlock釋放鎖時,便會調用這個方法。在該方法內部,首先通過 getEntryName() 獲取當前鎖實例的名稱,然后從 EXPIRATION_RENEWAL_MAP 中查找該名稱對應的 ExpirationEntry 對象。如果對象不存在,則直接返回即可;否則,如果傳入了調用者線程 ID,則從任務中移除該線程 ID。接著,如果線程 ID 為空,或者任務已經沒有任何線程在持有它了,則取消任務并從 EXPIRATION_RENEWAL_MAP 中刪除該實體。需要注意的是,取消定時任務的操作是通過調用 timeout.cancel() 實現的,它會將定時任務從時間輪中移除。由于 Redisson 是基于.NETty 的,所以它使用的是 HashedWheelTimer,這個定時器底層是基于時間輪實現的,并且支持動態添加和刪除定時任務。

三、 使用注意事項

使用Redisson分布式鎖的看門狗應注意以下幾個問題:

  • 設置合理的鎖超時時間:如果鎖的超時時間過短,則會導致頻繁續命和多次加鎖解鎖,影響程序性能;如果鎖的超時時間過長,則可能會因為某些異常原因使得鎖無法釋放,從而導致死鎖。
  • 在業務邏輯完成后及時啟動看門狗:如果業務邏輯執行時間過長,則有可能導致鎖的過期,從而使看門狗失去續命的意義。
  • 合理配置看門狗的參數:看門狗的續命時間間隔應該在鎖的過期時間內,且重試次數不宜過多,以免影響程序性能。
  • 避免鎖的嵌套使用:鎖的嵌套使用有可能導致死鎖或者其他并發問題,應避免使用。

分享到:
標簽:分布式
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定