切換語言為:簡體

Redis過期時間機制底層原理以及AOF、RDB和複製功能對過期鍵的處理方式

  • 爱糖宝
  • 2024-08-25
  • 2060
  • 0
  • 0

在 Redis 中,鍵的過期時間設計與實現是一個重要的功能,這使得 Redis 可以自動刪除在指定時間後不再需要的鍵。下面詳細介紹 Redis 過期時間的設計和實現,包括設定過期時間、過期鍵的儲存結構、過期鍵的刪除策略等。

1. 設定過期時間

Redis 提供了多個命令來設定鍵的過期時間,如 EXPIREPEXPIREEXPIREATPEXPIREAT。這些命令可以以秒或毫秒為單位設定鍵的過期時間,也可以設定具體的過期時間點。

  • EXPIRE key seconds

  • PEXPIRE key milliseconds

  • EXPIREAT key timestamp

  • PEXPIREAT key milliseconds-timestamp

示例:

void expireCommand(client *c) {
    long long seconds;
    if (getLongLongFromObjectOrReply(c, c->argv[2], &seconds, NULL) != C_OK)
        return;
    setExpire(c, c->db, c->argv[1], mstime() + seconds*1000);
    addReply(c, shared.cone);
}

2. 過期鍵的儲存結構

每個 Redis 資料庫例項(redisDb)中都有一個名為 expires 的字典,用於儲存鍵的過期時間。這個字典將鍵指標對映到以毫秒為單位的到期時間點。

typedef struct redisDb {
    dict *dict;                // 主字典,儲存所有鍵值對
    dict *expires;             // 過期字典,儲存鍵的過期時間
    ...
} redisDb;

3. 設定過期時間

透過 setExpire 函式設定鍵的過期時間。如果鍵已經存在於 expires 字典中,則更新其過期時間;否則,將其新增到 expires 字典中。

void setExpire(client *c, redisDb *db, robj *key, long long when) {
    dictEntry *de = dictFind(db->dict, key->ptr);
    if (de == NULL) return;

    /* Set the new expire time */
    if (dictAdd(db->expires, dictGetKey(de), (void*)when) == DICT_ERR) {
        dictReplace(db->expires, dictGetKey(de), (void*)when);
    }
}

4. 刪除過期鍵的策略

Redis 採用了以下三種策略來刪除過期鍵:

  1. 惰性刪除(Lazy Deletion) :每次訪問鍵時檢查其是否過期,如果已過期則刪除。這樣只在訪問鍵時才進行過期檢查,節省了資源。

    robj *lookupKeyRead(redisDb *db, robj *key) {
        robj *val;
        expireIfNeeded(db,key);  // 檢查並刪除過期鍵
        val = lookupKey(db,key,LOOKUP_NONE);
        return val ? val : NULL;
    }

  2. 定期刪除(Periodic Deletion) :Redis 會週期性地隨機抽取一定數量的鍵進行過期檢查,並刪除其中已過期的鍵。這一過程由後臺任務定期執行,確保儘可能多的過期鍵被及時刪除。

    int activeExpireCycle(int type) {
        unsigned int current_db = server.dbnum;
        long long start = ustime();
        long long timelimit = 1000000; // 1秒
        int dbs_per_call = CRON_DBS_PER_CALL;
    
        current_db = server.current_db;
        while(dbs_per_call--) {
            redisDb *db = server.db + (current_db % server.dbnum);
            activeExpireCycleTryExpire(db, cycle_tickets);
            current_db++;
        }
    
        long long elapsed = ustime()-start;
        return elapsed > timelimit;
    }

  3. 主動刪除(Active Expiration) :在記憶體使用接近最大限制時,會觸發主動刪除策略,透過掃描所有庫的鍵刪除過期資料,以確保記憶體使用量保持在設定範圍內。

    void evictExpiredKeys() {
        for (int j = 0; j < server.dbnum; j++) {
            redisDb *db = server.db+j;
            scanDatabaseForExpiredKeys(db);
        }
    }

    Redis 預設採用以下兩種刪除過期鍵策略:

    1. 惰性刪除(Lazy Deletion) :每次訪問某個鍵時檢查其是否過期,如果過期則刪除。

    2. 定期刪除(Periodic Deletion) :後臺任務定期掃描資料庫中的鍵,隨機抽取部分鍵進行過期檢查並刪除其中已過期的鍵。

5. 檢查並刪除過期鍵

expireIfNeeded 函式用於檢查某個鍵是否過期,如果過期則刪除該鍵。

int expireIfNeeded(redisDb *db, robj *key) {
    mstime_t when = getExpire(db, key);
    if (when < 0) return 0;

    if (mstime() > when) {
        server.stat_expiredkeys++;
        propagateExpire(db,key);
        dbDelete(db,key);
        return 1;
    } else {
        return 0;
    }
}

  • getExpire:從 expires 字典中獲取鍵的過期時間。

  • mstime:返回當前的毫秒時間戳。

  • 如果鍵已過期,則呼叫 dbDelete 刪除該鍵,並增加統計計數器 stat_expiredkeys

6. 獲取過期時間

getExpire 函式用於獲取鍵的過期時間,如果鍵沒有設定過期時間則返回 -1。

mstime_t getExpire(redisDb *db, robj *key) {
    dictEntry *de;
    if (dictSize(db->expires) == 0 ||
        (de = dictFind(db->expires, key->ptr)) == NULL) return -1;

    return (mstime_t)dictGetSignedIntegerVal(de);
}

總結

Redis 的過期時間設計與實現包括以下幾個關鍵點:

  1. 設定過期時間:透過 EXPIRE、PEXPIRE 等命令設定鍵的過期時間,並將過期時間儲存在 expires 字典中。

  2. 過期字典:每個資料庫例項都有一個 expires 字典,用於儲存鍵的過期時間。

  3. 刪除策略

    • 惰性刪除:每次訪問鍵時檢查其是否過期,如果已過期則刪除。

    • 定期刪除:透過後臺任務週期性地檢測並刪除過期鍵。

    • 主動刪除:在記憶體使用接近最大限制時觸發,掃描所有鍵並刪除過期鍵。

定期刪除activeExpireCycle函式詳細解析

void activeExpireCycle(int type) {
    static unsigned int current_db = 0;  // 記錄上一次處理的資料庫索引
    static int timelimit_exit = 0;       // 用於指示是否超出時間限制
    unsigned int j;
    // 每次要處理的資料庫數量
    unsigned int dbs_per_call = CRON_DBS_PER_CALL;
    long long start = ustime();          // 開始時間
    long long timelimit;                 // 時間限制

    if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
        /* Fast cycle: 1 ms */
        timelimit = 1000;
    } else {
        /* Slow cycle: 25% CPU time p. DB / Configurable percentage. */
        timelimit = server.hz < 100 ? 1000 : 10;
        if (server.active_expire_effort != 1)
            timelimit *= server.active_expire_effort-1;
        timelimit /= server.dbnum;
        timelimit_exit = 0;
    }

    for (j = 0; j < dbs_per_call; j++) {
        redisDb *db = server.db + (current_db % server.dbnum);
        current_db++;
        int expired, sampled;

        do {
            long now = mstime();
            expireEntry *de;
            dictEntry *d;

            /* Sample a few keys in the database */
            expired = 0;
            sampled = 0;
            while ((de = dictGetRandomKey(db->expires)) != NULL &&
                   mstime() - now < timelimit) {
                long long ttl = dictGetSignedIntegerVal(de) - mstime();
                if (ttl < 0) {
                    d = dictFind(db->dict, dictGetKey(de));
                    dbDelete(db, dictGetKey(d));
                    server.stat_expiredkeys++;
                    expired++;
                }
                sampled++;
            }
        } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP / 2);

        elapsed = ustime() - start;
        if (elapsed > timelimit) {
            timelimit_exit = 1;
            break;
        }
    }
}

關鍵步驟解析

1. 初始化變數

static unsigned int current_db = 0;
static int timelimit_exit = 0;
unsigned int j;
unsigned int dbs_per_call = CRON_DBS_PER_CALL;
long long start = ustime();
long long timelimit;

  • current_db:靜態變數,用於記錄上一次處理的資料庫索引。

  • timelimit_exit:用於指示是否耗盡了時間配額,防止無限迴圈。

  • dbs_per_call:每次掃描的資料庫數量,通常由配置決定。

  • start:記錄開始執行此函式的時間戳。

  • timelimit:本次呼叫允許消耗的最大時間(以微秒為單位)。

2. 確定時間限制

if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
    timelimit = 1000;  // 快速模式:1 毫秒
} else {
    timelimit = server.hz < 100 ? 1000 : 10;
    if (server.active_expire_effort != 1)
        timelimit *= server.active_expire_effort - 1;
    timelimit /= server.dbnum;
    timelimit_exit = 0;
}

  • 如果是快速模式,時間限制為 1 毫秒。

  • 如果是慢速模式,時間限制根據 Redis 配置和當前伺服器負載情況計算。

  • server.active_expire_effort 引數可以調整過期鍵清理的力度。

3. 遍歷資料庫

每次呼叫 activeExpireCycle 時,會遍歷一定數量的資料庫,並在每個資料庫中隨機抽取鍵進行過期檢查和刪除。

for (j = 0; j < dbs_per_call; j++) {
    redisDb *db = server.db + (current_db % server.dbnum);
    current_db++;
    int expired, sampled;

    do {
        long now = mstime();
        expireEntry *de;
        dictEntry *d;

        expired = 0;
        sampled = 0;
        while ((de = dictGetRandomKey(db->expires)) != NULL &&
               mstime() - now < timelimit) {
            long long ttl = dictGetSignedIntegerVal(de) - mstime();
            if (ttl < 0) {
                d = dictFind(db->dict, dictGetKey(de));
                dbDelete(db, dictGetKey(d));
                server.stat_expiredkeys++;
                expired++;
            }
            sampled++;
        }
    } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP / 2);

    elapsed = ustime() - start;
    if (elapsed > timelimit) {
        timelimit_exit = 1;
        break;
    }
}

關鍵點解析:

  • 選擇資料庫

    redisDb *db = server.db + (current_db % server.dbnum);
    current_db++;

    使用迴圈方式選擇下一個要檢查的資料庫例項。current_db 記錄上一次處理的資料庫索引,透過取模操作確保索引在有效範圍內。

  • 初始化變數

    int expired, sampled;
    expired = 0;
    sampled = 0;

  • 過期檢查迴圈

    while ((de = dictGetRandomKey(db->expires)) != NULL && mstime() - now < timelimit) {
        long long ttl = dictGetSignedIntegerVal(de) - mstime();
        if (ttl < 0) {
            d = dictFind(db->dict, dictGetKey(de));
            dbDelete(db, dictGetKey(d));
            server.stat_expiredkeys++;
            expired++;
        }
        sampled++;
    }

    • 從 expires 字典中隨機獲取一個鍵 de

    • 檢查當前時間是否超過了本次週期的時間限制 timelimit

    • 如果鍵已經過期(ttl < 0),則刪除該鍵,並增加已過期鍵的計數器 expired

    • 增加已檢查鍵的計數器 sampled

  • 多輪過期檢查

    do {
        ...
    } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP / 2);

    如果在一輪檢查中刪除的過期鍵數量超過預設值的一半,則繼續下一輪檢查。

4. 時間限制檢查

在每次處理完一個數據庫後,檢查是否超出時間限制:

elapsed = ustime() - start;
if (elapsed > timelimit) {
    timelimit_exit = 1;
    break;
}

  • 計算已耗費的時間 elapsed

  • 如果已耗費時間超過 timelimit,設定 timelimit_exit 為 1 並跳出迴圈。

總結

Redis 的 activeExpireCycle 函式透過以下步驟實現定期刪除過期鍵:

  1. 初始化變數並確定時間限制:根據當前的模式(快速或慢速)和配置引數,計算本次函式呼叫的時間限制。

  2. 遍歷資料庫:迴圈遍歷一定數量的資料庫。

  3. 過期檢查與刪除:從每個資料庫中隨機抽取鍵,檢查其是否過期並進行刪除,直到達到時間限制或刪除了一定數量的過期鍵。

  4. 時間限制檢查:確保函式不會超出規定的時間配額,以避免影響 Redis 的其他操作。

AOF、RDB和複製功能對過期鍵的處理

在 Redis 中,AOF(Append Only File)、RDB(Redis DataBase)和複製(Replication)功能對過期鍵的處理方式有所不同。下面詳細介紹這些機制如何處理過期鍵:

AOF 持久化

  • 記錄過期時間

    • 在 AOF 檔案中,除了寫入每個鍵值的設定操作外,還會寫入 EXPIRE 或 PEXPIRE 命令來記錄鍵的過期時間。

  • 重寫(Rewrite)過程

    • 當 AOF 檔案需要重寫時,Redis 會檢查每個鍵,如果鍵已過期,則不會將其寫入新的 AOF 檔案。

  • 載入 AOF 檔案

    • 當 Redis 重啟並載入 AOF 檔案時,會執行檔案中的所有命令,包括設定鍵值和設定過期時間的命令。如果某些鍵已經過期,這些鍵會立即被刪除。

RDB 持久化

  • 儲存快照

    • 當 Redis 建立 RDB 快照時,它會將所有鍵及其剩餘的過期時間一起儲存到快照檔案中。

  • 載入快照

    • 當 Redis 從 RDB 檔案恢復資料時,會載入所有鍵值對,同時載入它們的過期時間。如果某個鍵在載入時已經過期,Redis 會立即將其刪除。

複製(Replication)

  • 主從同步

    • 在主從複製架構中,主節點會將過期鍵的刪除操作傳播給從節點。

    • 如果一個鍵在主節點上過期並被刪除,主節點會向從節點發送 DEL 操作,從而在從節點上也刪除該鍵。

  • 延遲過期

    • 從節點可能因為網路延遲等原因,對過期鍵的處理會稍有滯後,但最終主從節點的資料將保持一致。

總結

  1. AOF:

    • 透過記錄 EXPIRE/PEXPIRE 命令來處理過期時間。

    • 在重寫過程中跳過已過期的鍵。

    • 載入時刪除已過期的鍵。

  2. RDB:

    • 將過期時間與鍵值一起儲存。

    • 載入時立即刪除已過期的鍵。

  3. 複製:

    • 主節點將刪除過期鍵的操作同步到從節點。

    • 確保主從節點資料的一致性。

0則評論

您的電子郵件等資訊不會被公開,以下所有項目均必填

OK! You can skip this field.