《Redis設計與實現》[第二部分]單機數據庫的實現-C源碼閱讀(一)

1、數據庫

關鍵字:鍵空間,過期,刪除策略

數據結構源碼

//redisServer中屬性太多,篇幅限制,故只列本章描述相關的屬性
struct redisServer {
    //...
    // 數據庫
                //一個數組,保存着服務器中的所有數據庫
    redisDb *db;
    // 服務器的數據庫數量
    int dbnum;          
    //..
} ;

Redis服務器將所有數據庫都保存在服務器狀態redis.h/redisServer結構的db數組中,db數組的每個項都是一個redis.h/redisDb結構,每個redisDb結構代表一個數據庫。

dbnum屬性的值由服務器配置的database項決定,默認爲16,所以Redis服務器默認會創建16個數據庫。

/* Redis database representation. There are multiple databases identified
 * by integers from 0 (the default database) up to the max configured
 * database. The database number is the 'id' field in the structure. */
typedef struct redisDb {

    // 數據庫鍵空間,保存着數據庫中的所有鍵值對
    dict *dict;                 /* The keyspace for this DB */

    // 鍵的過期時間,字典的鍵爲鍵,字典的值爲過期事件 UNIX 時間戳
            //過期字典,保存着鍵的過期時間
    dict *expires;              /* Timeout of keys with a timeout set */

    // 正處於阻塞狀態的鍵
    dict *blocking_keys;        /* Keys with clients waiting for data (BLPOP) */

    // 可以解除阻塞的鍵
    dict *ready_keys;           /* Blocked keys that received a PUSH */

    // 正在被 WATCH 命令監視的鍵
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */

    struct evictionPoolEntry *eviction_pool;    /* Eviction pool of keys */

    // 數據庫號碼
    int id;                     /* Database ID */

    // 數據庫的鍵的平均 TTL ,統計信息
    long long avg_ttl;          /* Average TTL, just for stats */

} redisDb;

每個Redis客戶端都有自己的目標數據庫,客戶端執行讀寫命令之前會先切換到目標數據庫,默認爲0號數據庫。

客戶端可以通過select命令切換目標數據庫。

typedef struct redisClient{
    //..
    //記錄客戶端當前正在使用的數據庫
    redisDb *db;
    //..
} redisClient;

db屬性是一個指向redisDb結構的指針,指向redisServer.db數組的其中一個元素,記錄了客戶端當前的目標數據庫。

目前爲止,Redis中沒有可以返回客戶端目標數據庫的命令,爲免誤操作,再執行寫命令之前,最好先執行一個select命令,顯式切換到指定數據庫。

typedef struct redisDb{
    //..
    // 數據庫鍵空間是一個字典,保存着數據庫中的所欲鍵值對
    dict *dict;
    //..
}redisDb;

鍵空間和用戶所見的數據庫是直接對應的:

  • 鍵空間的鍵就是數據庫的鍵,每個鍵都是一個字符串對象
  • 鍵空間的值就是數據庫的值,每個值可以是字符串對象、列表對象、哈希表對象、集合對象和有序集合對象中的任意一種Redis對象

使用Redis命令對數據庫進行讀寫時,服務器不僅會對鍵空間執行指定的讀寫操作,還會執行一些額外的維護操作,包括:

  • 讀取一個鍵後(讀操作和寫操作都要對鍵進行讀取),服務器會根據鍵是否存在來更新服務器的鍵空間命中(hit)次數或鍵空間不命中(miss)次數,這兩個值可以在info stats命令的keyspace_hits屬性和keyspace_misses屬性中查看

  • 在讀取一個鍵之後,服務器會更新鍵的LRU(最後一次使用)時間,該值用於計算鍵的閒置時間,使用Object idletime 命令可以查看鍵key的閒置時間

  • 如果服務器在讀取一個鍵時發現該鍵已經過期,那麼服務器會先刪除這個過期鍵,然後才執行餘下的其他操作

  • 如果有客戶端使用watch命令監視了某個鍵,那麼服務器在對被監視的鍵進行修改之後,會將這個鍵標記爲髒(dirty),從而讓事務程序注意到這個鍵已經被修改過

  • 服務器每次修改一個鍵之後,都會對髒(dirty)鍵計數器的值增1,這個計數器會觸發服務器的持久化與複製操作
  • 如果服務器開啓了數據庫通知功能,那麼在對鍵進行修改之後,服務器將按配置發送相應的數據庫通知。

生存與過期

通過expire與pexpire命令,客戶端可以以秒或毫秒精度爲數據庫中的某個鍵設置生存時間(Time To Live,TTL),在經過指定的秒數或者毫秒數之後,服務器就會自動刪除生存時間爲0的鍵

TTL命令與PTTL命令接受一個帶有生存時間或過期時間的鍵,返回這個鍵的剩餘生存時間,即,返回距離這個鍵被服務器自動刪除還有多長時間

typedef struct redisDb{
    //..
    // 過期字典,保存鍵的過期時間
    dict *expires;
    //..
}redisDb;

redisDb結構的expires字典保存了數據庫中所有鍵的過期時間,即過期字典:

  • 過期字典的鍵是一個指針,這個指針指向鍵空間的某個鍵對象(即某個數據庫鍵)
  • 過期字典的值是一個long long類型的整數,這個整數保存了鍵所指向的數據庫鍵的過期時間————一個毫秒精度的UNIX時間戳

通過過期字典,檢查給定鍵是否過期:

  1. 檢查給定鍵是否存在於過期字典,若存在,那麼取得鍵的過期時間
  2. 檢查當前UNIX時間戳是否大於鍵的過期時間:若是,則鍵已過期,否則未過期

過期鍵刪除策略

  • 定時刪除:在設置鍵的過期時間的同時,創建一個定時器(timer),讓定時器在鍵的過期時間來臨時,立即執行對鍵的刪除操作
    • 讓服務器創建大量定時器,實現定時刪除策略,佔用大量CPU時間,影響服務器的響應時間和吞吐量
  • 惰性刪除:放任過期鍵不管,每次從鍵空間取鍵時,檢查所取鍵是否過期,如果過期,刪除該鍵,若沒有過期,返回該鍵
    • 不主動釋放過期鍵,會造成內存的浪費,有內存泄漏的危險
  • 定期刪除:每隔一段時間,程序對數據庫進行依次檢查,刪除數據庫裏的過期鍵。
    • 難點在於確定刪除操作執行的時長和頻率

Redis服務器實際使用的是惰性刪除和定期刪除兩種策略:

通過配合使用這兩種刪除策略,服務器可以很好的在合理使用CPU時間和避免浪費內存空間之間取得平衡。

過期刪除函數

過期鍵的惰性刪除策略由db.c/expireIfNeeded函數實現。

/*
 * 檢查 key 是否已經過期,如果是的話,將它從數據庫中刪除。
 *
 * 返回 0 表示鍵沒有過期時間,或者鍵未過期。
 *
 * 返回 1 表示鍵已經因爲過期而被刪除了。
 */
int expireIfNeeded(redisDb *db, robj *key) {

    // 取出鍵的過期時間
    mstime_t when = getExpire(db,key);
    mstime_t now;

    // 沒有過期時間
    if (when < 0) return 0; /* No expire for this key */

    /* Don't expire anything while loading. It will be done later. */
    // 如果服務器正在進行載入,那麼不進行任何過期檢查
    if (server.loading) return 0;

    /* If we are in the context of a Lua script, we claim that time is
     * blocked to when the Lua script started. This way a key can expire
     * only the first time it is accessed and not in the middle of the
     * script execution, making propagation to slaves / AOF consistent.
     * See issue #1525 on Github for more information. */
    now = server.lua_caller ? server.lua_time_start : mstime();

    /* If we are running in the context of a slave, return ASAP:
     * the slave key expiration is controlled by the master that will
     * send us synthesized DEL operations for expired keys.
     *
     * Still we try to return the right information to the caller, 
     * that is, 0 if we think the key should be still valid, 1 if
     * we think the key is expired at this time. */
    // 當服務器運行在 replication 模式時
    // 附屬節點並不主動刪除 key
    // 它只返回一個邏輯上正確的返回值
    // 真正的刪除操作要等待主節點發來刪除命令時才執行
    // 從而保證數據的同步
    if (server.masterhost != NULL) return now > when;

    // 運行到這裏,表示鍵帶有過期時間,並且服務器爲主節點

    /* Return when this key has not expired */
    // 如果未過期,返回 0
    if (now <= when) return 0;

    /* Delete the key */
    server.stat_expiredkeys++;

    // 向 AOF 文件和附屬節點傳播過期信息
    propagateExpire(db,key);

    // 發送事件通知
    notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,
        "expired",key,db->id);

    // 將過期鍵從數據庫中刪除
    return dbDelete(db,key);
}

所有讀寫數據庫的Redis命令在執行之前都會調用該函數對輸入鍵檢查:

  • 如果輸入鍵已經過期,那麼expireIfNeeded函數將輸入鍵從數據庫中刪除
  • 如果輸入鍵未過期,那麼expireIfNeeded函數不做操作

定期刪除函數

過期鍵的定期刪除策略由redis.c/activeExpireCycle函數實現。
每當Redis的服務器週期性操作redis.c/serverCron函數執行時,actieExpireCycle函數就會被調用,它在規定的時間內,分多次遍歷服務器中的各個數據庫,從數據庫的expires字典中隨機檢查一部分鍵的過期時間,並刪除其中的過期鍵。

activeExpireCycle函數的工作模式總結如下:

  • 函數每次運行時,都從一定數量的數據庫中取出一定數量的隨機鍵進行檢查,並刪除其中的過期鍵
  • 全局變量current_db會記錄當前activeExpireCycle函數檢查的進度,並在下一次activeExpireCycle函數調用時,接着上一次的進度進行處理。
  • 隨着activeExpireCycle函數的不斷執行,服務器中所有數據庫都會被檢查一遍,這時函數將current_db變量重置爲0,然後再次開始新一輪的檢查工作
/* Try to expire a few timed out keys. The algorithm used is adaptive and
 * will use few CPU cycles if there are few expiring keys, otherwise
 * it will get more aggressive to avoid that too much memory is used by
 * keys that can be removed from the keyspace.
 *
 * 函數嘗試刪除數據庫中已經過期的鍵。
 * 當帶有過期時間的鍵比較少時,函數運行得比較保守,
 * 如果帶有過期時間的鍵比較多,那麼函數會以更積極的方式來刪除過期鍵,
 * 從而可能地釋放被過期鍵佔用的內存。
 *
 * No more than REDIS_DBCRON_DBS_PER_CALL databases are tested at every
 * iteration.
 *
 * 每次循環中被測試的數據庫數目不會超過 REDIS_DBCRON_DBS_PER_CALL 。
 *
 * This kind of call is used when Redis detects that timelimit_exit is
 * true, so there is more work to do, and we do it more incrementally from
 * the beforeSleep() function of the event loop.
 *
 * 如果 timelimit_exit 爲真,那麼說明還有更多刪除工作要做,
 * 那麼在 beforeSleep() 函數調用時,程序會再次執行這個函數。
 *
 * Expire cycle type:
 *
 * 過期循環的類型:
 *
 * If type is ACTIVE_EXPIRE_CYCLE_FAST the function will try to run a
 * "fast" expire cycle that takes no longer than EXPIRE_FAST_CYCLE_DURATION
 * microseconds, and is not repeated again before the same amount of time.
 *
 * 如果循環的類型爲 ACTIVE_EXPIRE_CYCLE_FAST ,
 * 那麼函數會以“快速過期”模式執行,
 * 執行的時間不會長過 EXPIRE_FAST_CYCLE_DURATION 毫秒,
 * 並且在 EXPIRE_FAST_CYCLE_DURATION 毫秒之內不會再重新執行。
 *
 * If type is ACTIVE_EXPIRE_CYCLE_SLOW, that normal expire cycle is
 * executed, where the time limit is a percentage of the REDIS_HZ period
 * as specified by the REDIS_EXPIRELOOKUPS_TIME_PERC define. 
 *
 * 如果循環的類型爲 ACTIVE_EXPIRE_CYCLE_SLOW ,
 * 那麼函數會以“正常過期”模式執行,
 * 函數的執行時限爲 REDIS_HS 常量的一個百分比,
 * 這個百分比由 REDIS_EXPIRELOOKUPS_TIME_PERC 定義。
 */

void activeExpireCycle(int type) {
    /* This function has some global state in order to continue the work
     * incrementally across calls. */
    // 靜態變量,用來累積函數連續執行時的數據
    static unsigned int current_db = 0; /* Last DB tested. */
    static int timelimit_exit = 0;      /* Time limit hit in previous call? */
    static long long last_fast_cycle = 0; /* When last fast cycle ran. */

    unsigned int j, iteration = 0;
    // 默認每次處理的數據庫數量
    unsigned int dbs_per_call = REDIS_DBCRON_DBS_PER_CALL;
    // 函數開始的時間
    long long start = ustime(), timelimit;

    // 快速模式
    if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
        /* Don't start a fast cycle if the previous cycle did not exited
         * for time limt. Also don't repeat a fast cycle for the same period
         * as the fast cycle total duration itself. */
        // 如果上次函數沒有觸發 timelimit_exit ,那麼不執行處理
        if (!timelimit_exit) return;
        // 如果距離上次執行未夠一定時間,那麼不執行處理
        if (start < last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*2) return;
        // 運行到這裏,說明執行快速處理,記錄當前時間
        last_fast_cycle = start;
    }

    /* We usually should test REDIS_DBCRON_DBS_PER_CALL per iteration, with
     * two exceptions:
     *
     * 一般情況下,函數只處理 REDIS_DBCRON_DBS_PER_CALL 個數據庫,
     * 除非:
     *
     * 1) Don't test more DBs than we have.
     *    當前數據庫的數量小於 REDIS_DBCRON_DBS_PER_CALL
     * 2) If last time we hit the time limit, we want to scan all DBs
     * in this iteration, as there is work to do in some DB and we don't want
     * expired keys to use memory for too much time. 
     *     如果上次處理遇到了時間上限,那麼這次需要對所有數據庫進行掃描,
     *     這可以避免過多的過期鍵佔用空間
     */
    if (dbs_per_call > server.dbnum || timelimit_exit)
        dbs_per_call = server.dbnum;

    /* We can use at max ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC percentage of CPU time
     * per iteration. Since this function gets called with a frequency of
     * server.hz times per second, the following is the max amount of
     * microseconds we can spend in this function. */
    // 函數處理的微秒時間上限
    // ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 默認爲 25 ,也即是 25 % 的 CPU 時間
    timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;
    timelimit_exit = 0;
    if (timelimit <= 0) timelimit = 1;

    // 如果是運行在快速模式之下
    // 那麼最多隻能運行 FAST_DURATION 微秒 
    // 默認值爲 1000 (微秒)
    if (type == ACTIVE_EXPIRE_CYCLE_FAST)
        timelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION; /* in microseconds. */

    // 遍歷數據庫
    for (j = 0; j < dbs_per_call; j++) {
        int expired;
        // 指向要處理的數據庫
        redisDb *db = server.db+(current_db % server.dbnum);

        /* Increment the DB now so we are sure if we run out of time
         * in the current DB we'll restart from the next. This allows to
         * distribute the time evenly across DBs. */
        // 爲 DB 計數器加一,如果進入 do 循環之後因爲超時而跳出
        // 那麼下次會直接從下個 DB 開始處理
        current_db++;

        /* Continue to expire if at the end of the cycle more than 25%
         * of the keys were expired. */
        do {
            unsigned long num, slots;
            long long now, ttl_sum;
            int ttl_samples;

            /* If there is nothing to expire try next DB ASAP. */
            // 獲取數據庫中帶過期時間的鍵的數量
            // 如果該數量爲 0 ,直接跳過這個數據庫
            if ((num = dictSize(db->expires)) == 0) {
                db->avg_ttl = 0;
                break;
            }
            // 獲取數據庫中鍵值對的數量
            slots = dictSlots(db->expires);
            // 當前時間
            now = mstime();

            /* When there are less than 1% filled slots getting random
             * keys is expensive, so stop here waiting for better times...
             * The dictionary will be resized asap. */
            // 這個數據庫的使用率低於 1% ,掃描起來太費力了(大部分都會 MISS)
            // 跳過,等待字典收縮程序運行
            if (num && slots > DICT_HT_INITIAL_SIZE &&
                (num*100/slots < 1)) break;

            /* The main collection cycle. Sample random keys among keys
             * with an expire set, checking for expired ones. 
             *
             * 樣本計數器
             */
            // 已處理過期鍵計數器
            expired = 0;
            // 鍵的總 TTL 計數器
            ttl_sum = 0;
            // 總共處理的鍵計數器
            ttl_samples = 0;

            // 每次最多隻能檢查 LOOKUPS_PER_LOOP 個鍵
            if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
                num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;

            // 開始遍歷數據庫
            while (num--) {
                dictEntry *de;
                long long ttl;

                // 從 expires 中隨機取出一個帶過期時間的鍵
                if ((de = dictGetRandomKey(db->expires)) == NULL) break;
                // 計算 TTL
                ttl = dictGetSignedIntegerVal(de)-now;
                // 如果鍵已經過期,那麼刪除它,並將 expired 計數器增一
                if (activeExpireCycleTryExpire(db,de,now)) expired++;
                if (ttl < 0) ttl = 0;
                // 累積鍵的 TTL
                ttl_sum += ttl;
                // 累積處理鍵的個數
                ttl_samples++;
            }

            /* Update the average TTL stats for this database. */
            // 爲這個數據庫更新平均 TTL 統計數據
            if (ttl_samples) {
                // 計算當前平均值
                long long avg_ttl = ttl_sum/ttl_samples;

                // 如果這是第一次設置數據庫平均 TTL ,那麼進行初始化
                if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
                /* Smooth the value averaging with the previous one. */
                // 取數據庫的上次平均 TTL 和今次平均 TTL 的平均值
                db->avg_ttl = (db->avg_ttl+avg_ttl)/2;
            }

            /* We can't block forever here even if there are many keys to
             * expire. So after a given amount of milliseconds return to the
             * caller waiting for the other active expire cycle. */
            // 我們不能用太長時間處理過期鍵,
            // 所以這個函數執行一定時間之後就要返回

            // 更新遍歷次數
            iteration++;

            // 每遍歷 16 次執行一次
            if ((iteration & 0xf) == 0 && /* check once every 16 iterations. */
                (ustime()-start) > timelimit)
            {
                // 如果遍歷次數正好是 16 的倍數
                // 並且遍歷的時間超過了 timelimit
                // 那麼斷開 timelimit_exit
                timelimit_exit = 1;
            }

            // 已經超時了,返回
            if (timelimit_exit) return;

            /* We don't repeat the cycle if there are less than 25% of keys
             * found expired in the current DB. */
            // 如果已刪除的過期鍵佔當前總數據庫帶過期時間的鍵數量的 25 %
            // 那麼不再遍歷
        } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
    }
}

RDB對過期鍵的處理

在執行save命令或者BGSAVE命令創建一個新的RDB文件時,程序會對數據庫中的鍵進行檢查,已過期的鍵不會被保存到新創建的RDB文件中。

所以,數據庫中包含過期鍵不會對生存新的RDB文件造成影響。

在啓動Redis服務器時,若服務器開啓了RDB功能,那麼服務器將對RDB文件進行載入:

  • 如果服務器以主服務器模式運行,那麼載入RDB文件時,程序會對文件中保存的鍵進行檢查,未過期的鍵會被載入到數據庫中,而過期鍵會被忽略,所以過期鍵對載入RDB的主服務器不會造成影響
  • 若服務器以從服務器模式運行,那麼載入RDB時,文件中保存的所有鍵,不論是否過期,都會被載入到數據庫中。但是,因爲主從服務器在進行數據同步的時候,從服務器的數據庫就會被清空,所以,過期鍵對載入RDB文件的從服務器也不會造成影響

AOF對過期鍵的處理

在執行AOF重寫時,程序會對數據庫中的鍵進行檢查,已過期的鍵不會被保存到重寫後的AOF文件中。

複製對過期鍵的處理

當服務器運行在複製模式下時,從服務器的過期鍵刪除動作由主服務器控制:

  • 主服務器在刪除一個過期鍵之後,會顯式地向所有從服務器發送一個del命令,告知從服務器刪除這個過期鍵
  • 從服務器在執行客戶端發送的讀命令時,即使碰到過期鍵也不會將過期鍵刪除,而是繼續像處理未過期鍵一樣處理過期鍵
  • 從服務器只有在接到主服務器發來的del命令之後,纔會刪除過期鍵。

數據庫通知

數據庫通知可以讓客戶端通過訂閱給定的頻道或模式,來獲知數據庫中鍵的變化,以及數據庫中命令的執行情況。

數據庫通知分爲兩類:

  • 鍵空間通知(key-space notification):某個鍵執行了什麼命令
  • 鍵事件通知(key-event notification):某個命令被什麼鍵執行了

服務器配置的notify-keyspace-events選項決定了服務器所發送通知的類型:

  • 所有類型的鍵空間和鍵事件通知:AKE
  • 所有類型的鍵空間通知:AK
  • 所有類型的鍵事件通知:AE
  • 字符串類型的鍵空間通知:K$
  • 列表類型的鍵事件通知:El

發送數據庫通知的功能是由notify.c/notifyKeyspaceEvent函數實現的:
void notifyKeyspaceEvent(int type, char *event, robj *key, int dbid);

  • type參數是當前想要發送的通知的類型,程序會根據這個值判斷通知是否就是服務器配置notify-keyspace-events選項所選定的通知類型,從而決定是否發送通知。

    • event:事件的名稱
  • keys:產生事件的鍵

  • dbid:產生事件的數據庫號碼

函數會根據type參數以及這三個參數構建事件通知的內容,以及接收通知的頻道名

每當一個Redis命令需要發送數據庫通知的時候,該命令的實現函數就會調用notifyKeyspaceEvent函數,並向函數傳遞該命令所引發的事件的相關信息。

notifyKeyspaceEvent函數執行以下操作:

  1. server.notify_keyspace_events屬性就是服務器配置notify_keyspace_events選項所設置的值,如果給定的通知類型type不是服務器允許發送的通知類型,那麼函數會直接返回,不做任何動作。
  2. 如果給定的通知是服務器允許發送的通知,那麼下一步函數會檢測服務器是否允許發送鍵空間通知,若允許,程序就會構建併發送事件通知。
  3. 最後,函數檢測服務器是否允許發送鍵事件通知,若允許,程序就會構建併發送事件通知
/* The API provided to the rest of the Redis core is a simple function:
 *
 * notifyKeyspaceEvent(char *event, robj *key, int dbid);
 *
 * 'event' is a C string representing the event name.
 *
 * event 參數是一個字符串表示的事件名
 *
 * 'key' is a Redis object representing the key name.
 *
 * key 參數是一個 Redis 對象表示的鍵名
 *
 * 'dbid' is the database ID where the key lives.  
 *
 * dbid 參數爲鍵所在的數據庫
 */
void notifyKeyspaceEvent(int type, char *event, robj *key, int dbid) {
    sds chan;
    robj *chanobj, *eventobj;
    int len = -1;
    char buf[24];

    /* If notifications for this class of events are off, return ASAP. */
    // 如果服務器配置爲不發送 type 類型的通知,那麼直接返回
    if (!(server.notify_keyspace_events & type)) return;

    // 事件的名字
    eventobj = createStringObject(event,strlen(event));

    /* __keyspace@<db>__:<key> <event> notifications. */
    // 發送鍵空間通知
    if (server.notify_keyspace_events & REDIS_NOTIFY_KEYSPACE) {

        // 構建頻道對象
        chan = sdsnewlen("__keyspace@",11);
        len = ll2string(buf,sizeof(buf),dbid);
        chan = sdscatlen(chan, buf, len);
        chan = sdscatlen(chan, "__:", 3);
        chan = sdscatsds(chan, key->ptr);

        chanobj = createObject(REDIS_STRING, chan);

        // 通過 publish 命令發送通知
        pubsubPublishMessage(chanobj, eventobj);

        // 釋放頻道對象
        decrRefCount(chanobj);
    }

    /* __keyevente@<db>__:<event> <key> notifications. */
    // 發送鍵事件通知
    if (server.notify_keyspace_events & REDIS_NOTIFY_KEYEVENT) {

        // 構建頻道對象
        chan = sdsnewlen("__keyevent@",11);
        // 如果在前面發送鍵空間通知的時候計算了 len ,那麼它就不會是 -1
        // 這可以避免計算兩次 buf 的長度
        if (len == -1) len = ll2string(buf,sizeof(buf),dbid);
        chan = sdscatlen(chan, buf, len);
        chan = sdscatlen(chan, "__:", 3);
        chan = sdscatsds(chan, eventobj->ptr);

        chanobj = createObject(REDIS_STRING, chan);

        // 通過 publish 命令發送通知
        pubsubPublishMessage(chanobj, key);

        // 釋放頻道對象
        decrRefCount(chanobj);
    }

    // 釋放事件對象
    decrRefCount(eventobj);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章