Redis數據庫


Redis數據庫

 

1.Redis服務器

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

struct redisServer {
    // ...
    //
    一個數組,保存着服務器中的所有數據庫
    redisDb *db;
    // ...
   //服務器的數據庫數量
    int dbnum;
};

dbnum屬性的值由服務器配置的database選項決定,默認情況下,該選項的值為16,所以Redis服務器默認會創建16個數據庫

默認情況下,Redis客戶端的目標數據庫為0號數據庫,但客戶端可以通過執行SELECT命令來切換目標數據庫

 

2.Redis客戶端

在服務器內部,客戶端狀態redisClient結構的db屬性記錄了客戶端當前的目標數據庫,這個屬性是一個指向redisDb結構的指針:

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

3.Redis數據庫

typedef struct redisDb {
    dict *dict;                   /* 當前數據庫的鍵空間 */
    dict *expires;                /* 鍵的過期時間 */
    dict *blocking_keys;    /* 處於阻塞狀態的鍵和相應的client(主要用於List類型的阻塞操作)*/
    dict *ready_keys;        /* 准備好數據可以解除阻塞狀態的鍵和相應的client */
    dict *watched_keys;     /* 被watch命令監控的key和相應client */
    int id;                          /* 數據庫ID標識 */
    long long avg_ttl;         /* 數據庫內所有鍵的平均TTL(生存時間) */
    list *defrag_later;         /*逐一嘗試整理碎片的關鍵名稱列表 */
} redisDb;

redisDb結構的dict字典保存了數據庫中的所有鍵值對,我們將這個字典稱為鍵空間(key space) 鍵空間和用戶所見的數據庫是直接對應的:

❑鍵空間的鍵也就是數據庫的鍵,每個鍵都是一個字符串對象。

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

 

4.Redis鍵過期時間和過期時間查詢TTL

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

expires字段也是一個字典dict結構,字典的鍵為key,值為該key對應的過期時間,過期時間為long long類型整數,是以毫秒為單位的過期 UNIX 時間戳。setExpire函數的作用是為指定key設置過期時間。

/* 為指定key設置過期時間 */
void setExpire(redisDb *db, robj *key, long long when) {
    dictEntry *kde, *de;

    /* Reuse the sds from the main dict in the expire dict */
    // db->dict和db->expires是共用key字符串對象的
    // 取出key
    kde = dictFind(db->dict,key->ptr);
    redisAssertWithInfo(NULL,key,kde != NULL);
    // 取出過期時間
    de = dictReplaceRaw(db->expires,dictGetKey(kde));
    // 重置key的過期時間
    dictSetSignedIntegerVal(de,when);
}

有了過期時間戳我們就很容易判斷某個key是否過期:只要將當前時間戳跟過期時間戳比較一下即可,如果當前時間戳大於過期時間戳顯然該key已經過期了。

在Redis中,如果沒有為一個key設置過期時間,那么該key就不會出現在db->expires字典中。也就是說db->expires字段只保存了設置有過期時間的key。

  • 設置過期時間

Redis有四個不同的命令可以用於設置鍵的生存時間(鍵可以存在多久)或過期時間(鍵什么時候會被刪除):(expire.c中)

  ❑EXPIRE<key><ttl>命令用於將鍵key的生存時間設置為ttl秒

  ❑PEXPIRE<key><ttl>命令用於將鍵key的生存時間設置為ttl毫秒

  ❑EXPIREAT<key><timestamp>命令用於將鍵key的過期時間設置為timestamp所指定的秒數時間戳

  ❑PEXPIREAT<key><timestamp>命令用於將鍵key的過期時間設置為timestamp所指定的毫秒數時間戳

EXPIREAT命令與EXPIRE命令的差別在於前者使用Unix時間作為第二個參數表示鍵的生存時間的截止時間。PEXPIREAT命令與EXPIREAT命令的區別是前者的時間單位是毫秒。

雖然有多種不同單位和不同形式的設置命令,但實際上EXPIRE、PEXPIRE、EXPIREAT三個命令都是使用PEXPIREAT命令來實現的:無論客戶端執行的是以上四個命令中的哪一個,經過轉換之后,最終的執行效果都和執行PEXPIREAT命令一樣。

/* EXPIRE key seconds */
void expireCommand(client *c) {
    expireGenericCommand(c,mstime(),UNIT_SECONDS);
}

/* EXPIREAT key time */
void expireatCommand(client *c) {
    expireGenericCommand(c,0,UNIT_SECONDS);
}

/* PEXPIRE key milliseconds */
void pexpireCommand(client *c) {
    expireGenericCommand(c,mstime(),UNIT_MILLISECONDS);
}

/* PEXPIREAT key ms_time */
void pexpireatCommand(client *c) {
    expireGenericCommand(c,0,UNIT_MILLISECONDS);
}


void expireGenericCommand(redisClient *c, long long basetime, int unit) {
    robj *key = c->argv[1], *param = c->argv[2];
    // 以毫秒為單位的unix時間戳
    long long when;    // 獲取過期時間
    if (getLongLongFromObjectOrReply(c, param, &when, NULL) != REDIS_OK)
        return;

    // 如果傳入的過期時間是以秒為單位,則轉換為毫秒為單位
    if (unit == UNIT_SECONDS) when *= 1000;
    // 加上basetime得到過期時間戳
    when += basetime;

    /* No key, return zero. */
    // 取出key,如果該key不存在直接返回
    if (lookupKeyRead(c->db,key) == NULL) {
        addReply(c,shared.czero);
        return;
    }
    if (when <= mstime() && !server.loading && !server.masterhost) {
        // 如果when指定的時間已經過期,而且當前為服務器的主節點,並且目前沒有載入數據
        robj *aux;
        redisAssertWithInfo(c,key,dbDelete(c->db,key));
        server.dirty++;
        // 傳播一個顯式的DEL命令
        aux = createStringObject("DEL",3);
        rewriteClientCommandVector(c,2,aux,key);
        decrRefCount(aux);
        signalModifiedKey(c->db,key);
        notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"del",key,c->db->id);
        addReply(c, shared.cone);
        return;
    } else {
        // 設置key的過期時間(when提供的時間可能已經過期)
        setExpire(c->db,key,when);
        addReply(c,shared.cone);
        signalModifiedKey(c->db,key);                  notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"expire",key,c->db->id);
        server.dirty++;
        return;
    }
}
  • TTL命令

TTL命令以秒為單位返回鍵的剩余生存時間,而PTTL命令則以毫秒為單位返回鍵的剩余生存時間

/* TTL key */
void ttlCommand(client *c) {
    ttlGenericCommand(c, 0);
}

/* PTTL key */
void pttlCommand(client *c) {
    ttlGenericCommand(c, 1);
}

void ttlGenericCommand(client *c, int output_ms) {
    long long expire, ttl = -1;

    /* 如果這個鍵不存在 return -2 */
    if (lookupKeyReadWithFlags(c->db,c->argv[1],LOOKUP_NOTOUCH) == NULL) {
        addReplyLongLong(c,-2);
        return;
    }
    /* 鍵存在. Return -1 if 已經過期, or the 實際的TTL值otherwise. */
    expire = getExpire(c->db,c->argv[1]);
    if (expire != -1) {
        ttl = expire-mstime();
        if (ttl < 0) ttl = 0;
    }
    if (ttl == -1) {
        addReplyLongLong(c,-1);
    } else {
        addReplyLongLong(c,output_ms ? ttl : ((ttl+500)/1000));
    }
}
  • 過期鍵刪除策略

如果一個鍵過期了,那么它什么時候會被刪除呢?這個問題有三種可能的答案,它們分別代表了三種不同的刪除策略:

  ❑定時刪除:在設置鍵的過期時間的同時,創建一個定時器(timer),讓定時器在鍵的過期時間來臨時,立即執行對鍵的刪除操作。

  ❑惰性刪除:放任鍵過期不管,但是每次從鍵空間中獲取鍵時,都檢查取得的鍵是否過期,如果過期的話,就刪除該鍵;如果沒有過期,就返回該鍵。

  ❑定期刪除:每隔一段時間,程序就對數據庫進行一次檢查,刪除里面的過期鍵。至於要刪除多少過期鍵,以及要檢查多少個數據庫,則由算法決定。

在這三種策略中,第一種和第三種為主動刪除策略,而第二種則為被動刪除策略。

 

1)過期鍵的惰性刪除策略由db.c/expireIfNeeded函數實現,所有讀寫數據庫的Redis命令在執行之前都會調用expireIfNeeded函數對輸入鍵進行檢查:

  ❑如果輸入鍵已經過期,那么expireIfNeeded函數將輸入鍵從數據庫中刪除。

  ❑如果輸入鍵未過期,那么expireIfNeeded函數不做動作。

對於過期的key,Redis負責將該key刪除,為了提高運行效率,Redis采取這么一種處理方式:只有當真正要訪問該key時才檢查該key是否過期。如果過期就刪除,如果沒過期就正常訪問。通常我們把這種只有在訪問時才檢查過期的策略叫做“惰性刪除”。

int expireIfNeeded(redisDb *db, robj *key) {
    // 獲取key的過期時間
    mstime_t when = getExpire(db,key);
    mstime_t now;

    // 如果該key沒有過期時間,返回0
  if (when < 0) return 0; 

    // 如果服務器正在加載操作中,則不進行過期檢查,返回0
  if (server.loading) return 0;

    //如果我們處在Lua腳本的上下文中,我們假設直到Lua腳本啟動時間不變的。 
    //通過這種方式,key只能在第一次訪問而不是在腳本執行過程中過期,
    //從而使slave/AOF傳播一致。
  now = server.lua_caller ? server.lua_time_start : mstime();

    // 如果當前程序運行在slave節點,該key的過期操作是由master節點控制的(master節點會發出DEL操作)
    // 在這種情況下該函數先返回一個正確值,即如果key未過期返回0,否則返回1。
    // 真正的刪除操作等待master節點發來的DEL命令后再執行
    if (server.masterhost != NULL) return now > when;

    // 如果未過期,返回0
    if (now <= when) return 0;

    // 如果已過期,刪除該key
    server.stat_expiredkeys++;
    propagateExpire(db,key,server.lazyfree_lazy_expire);
    notifyKeyspaceEvent(NOTIFY_EXPIRED,
        "expired",key,db->id);
    return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) : dbSyncDelete(db,key);
}

對於過期key,Redis主(master)節點和附屬(slave)節點有不同的處理策略,具體如下:

如果當前Redis服務器是主節點,即if (server.masterhost != NULL)語句判斷為false,那么當它發現一個過期key后,會調用propagateExpire函數向所有附屬節點發送一個 DEL 命令,然后再刪除該key。這種做法使得對key的過期操作可以集中在一個地方處理。

如果當前Redis服務器是附屬節點,即if (server.masterhost != NULL)語句判斷為true,那么它立即向程序返回該key是否已經過期的信息。即便該key已經過期也不會真正的刪除該key。直到該節點接到從主節點發來的DEL 命令之后,才會真正執行刪除操作。

當Redis從數據庫db中取出指定key的對象時,總是先調用調用expireIfNeeded函數來檢查對應key是否過期,然后再從數據庫中查找對象。

robj *lookupKeyRead(redisDb *db, robj *key) {
    return lookupKeyReadWithFlags(db,key,LOOKUP_NONE);
}

robj *lookupKeyReadWithFlags (redisDb *db, robj *key, int flags) {
    robj *val;
    // 如果key已過期,刪除該key
    if (expireIfNeeded(db,key) == 1) {
        /*密鑰過期, 如果當前為master,expireIfNeeded(),僅當密鑰不存在時才返回0,所以它很安全,盡快返回NULL*/
        if (server.masterhost == NULL) return NULL;

        /* 如果當前處於slave節點,expireIfNeeded只返回信息*/
        if (server.current_client &&
            server.current_client != server.master &&
            server.current_client->cmd &&
            server.current_client->cmd->flags & CMD_READONLY)
        {
            return NULL;
        }
}    
// 從數據庫db中找到指定key的對象
    val = lookupKey(db,key, flags);
    if (val == NULL)
        // 更新“未命中”次數
        server.stat_keyspace_misses++;
    else
        // 更新“命中”次數
        server.stat_keyspace_hits++;
    return val;
}
/*  該函數是為寫操作而從數據庫db中取出指定key的對象。
    如果敢函數執行成功則返回目標對象,否則返回NULL。*/
robj *lookupKeyWrite(redisDb *db, robj *key) {
    // 如果key已過期,刪除該key
expireIfNeeded(db,key);
// 從數據庫db中找到指定key的對象
    return lookupKey(db,key,LOOKUP_NONE);
}

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

//周期性操作中進行慢速過期鍵刪除,執行頻率同databasesCron的執行頻率  
//執行時長為1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100  
void databasesCron(void) {  
    if (server.active_expire_enabled && server.masterhost == NULL) {  
        activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);  
    } else if (server.masterhost != NULL) {  
        expireSlaveKeys();  
    }  
}//進行快速過期鍵刪除,執行間隔和執行時長都為ACTIVE_EXPIRE_CYCLE_FAST_DURATION  
void beforeSleep(struct aeEventLoop *eventLoop) {  
    if (server.active_expire_enabled && server.masterhost == NULL)  
        activeExpireCycle(ACTIVE_EXPIRE_CYCLE_FAST);  
}  
void activeExpireCycle(int type) {
  static unsigned int current_db = 0; /* 上次定期刪除遍歷到的數據庫id*/
  static int timelimit_exit = 0;      /* Time limit hit in previous call? */
  static long long last_fast_cycle = 0; /* 上一次執行快速定期刪除的時間點 */

  int j, iteration = 0;
  int dbs_per_call = CRON_DBS_PER_CALL;//每次定期刪除,遍歷的數據庫的數量
  long long start = ustime(), timelimit;   /*當客戶端暫停時,數據集應該是靜態的,不僅僅客戶端的命令無法寫入的,而且key到期的指令無法執行*/   if (clientsArePaused()) return;   if (type == ACTIVE_EXPIRE_CYCLE_FAST) { if (!timelimit_exit) return; //快速定期刪除的時間間隔是ACTIVE_EXPIRE_CYCLE_FAST_DURATION //ACTIVE_EXPIRE_CYCLE_FAST_DURATION是快速定期刪除的執行時長 if (start < last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*2) return; last_fast_cycle = start;   }   //我們通常應該每次迭代測試CRON_DBS_PER_CALL   if (dbs_per_call > server.dbnum || timelimit_exit) dbs_per_call = server.dbnum; //慢速定期刪除的執行時長 timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100; timelimit_exit = 0; if (timelimit <= 0) timelimit = 1; if (type == ACTIVE_EXPIRE_CYCLE_FAST) timelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION; /*刪除操作的執行時長 */ //當我們將密鑰過期時,累積一些全局統計信息,以了解已經邏輯過期但仍存在於數據庫內的密鑰數量。   long total_sampled = 0;
  long total_expired = 0;   
  for (j = 0; j < dbs_per_call; j++) {     int expired;     redisDb *db = server.db+(current_db % server.dbnum);     current_db++;     do {       unsigned long num, slots;       long long now, ttl_sum;       int ttl_samples;       iteration++;       //如果沒有要刪除的鍵就轉向下一個數據庫       if ((num = dictSize(db->expires)) == 0) {         db->avg_ttl = 0;         break;       }       slots = dictSlots(db->expires);       now = mstime();       //當槽的填充小於1%,key顯得很重要,因此會等待更好的時機進行鍵清除。       if (num && slots > DICT_HT_INITIAL_SIZE && (num*100/slots < 1)) break;     }   }   expired = 0;   ttl_sum = 0;   ttl_samples = 0;   //在每個數據庫中檢查的鍵的數量   if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)     num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;   //從db->expires中隨機選取num個鍵進行檢查   while (num--) {     if ((de = dictGetRandomKey(db->expires)) == NULL) break;       ttl = dictGetSignedIntegerVal(de)-now;       //過期檢查,並對過期鍵進行刪除       if (activeExpireCycleTryExpire(db,de,now)) expired++;         if (ttl > 0) {         /* We want the average TTL of keys yet not expired. */ ttl_sum += ttl; ttl_samples++; } total_sampled++; }       //更新平均過期時間 if (ttl_samples) { long long avg_ttl = ttl_sum/ttl_samples; //用幾個樣本做一個簡單的運行平均值。我們只使用當前的估計值,權重為2%和以前的估計98%的權重。 if (db->avg_ttl == 0) db->avg_ttl = avg_ttl; db->avg_ttl = (db->avg_ttl/50)*49 + (avg_ttl/50);       }       //即使有很多密鑰過期,我們也不能永遠阻止。 因此,給定的毫秒數量返回給調用者,等待另一個活動的過期周期。       if ((iteration & 0xf) == 0) { /* check once every 16 iterations. */         elapsed = ustime()-start;         if (elapsed > timelimit) {           timelimit_exit = 1;           server.stat_expired_time_cap_reached_count++;           break;         }       } } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4); //每次檢查只刪除ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4個過期鍵 } }

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

❑函數每次運行時,都從一定數量的數據庫中取出一定數量的隨機鍵進行檢查,並刪除其中的過期鍵。

❑全局變量current_db會記錄當前activeExpireCycle函數檢查的進度,並在下一次activeExpireCycle函數調用時,接着上一次的進度進行處理。比如說,如果當前activeExpireCycle函數在遍歷10號數據庫時返回了,那么下次activeExpireCycle函數執行時,將從11號數據庫開始查找並刪除過期鍵。

❑隨着activeExpireCycle函數的不斷執行,服務器中的所有數據庫都會被檢查一遍,這時函數將current_db變量重置為0,然后再次開始新一輪的檢查工作。

 

總結:

❑Redis服務器的所有數據庫都保存在redisServer.db數組中,而數據庫的數量則由redisServer.dbnum屬性保存。

❑客戶端通過修改目標數據庫指針,讓它指向redisServer.db數組中的不同元素來切換不同的數據庫。

❑數據庫主要由dict和expires兩個字典構成,其中dict字典負責保存鍵值對,而expires字典則負責保存鍵的過期時間。

❑因為數據庫由字典構成,所以對數據庫的操作都是建立在字典操作之上的。

❑數據庫的鍵總是一個字符串對象,而值則可以是任意一種Redis對象類型,包括字符串對象、哈希表對象、集合對象、列表對象和有序集合對象,分別對應字符串鍵、哈希表鍵、集合鍵、列表鍵和有序集合鍵。

❑expires字典的鍵指向數據庫中的某個鍵,而值則記錄了數據庫鍵的過期時間,過期時間是一個以毫秒為單位的UNIX時間戳。

❑Redis使用惰性刪除和定期刪除兩種策略來刪除過期的鍵:惰性刪除策略只在碰到過期鍵時才進行刪除操作,定期刪除策略則每隔一段時間主動查找並刪除過期鍵。

❑執行SAVE命令或者BGSAVE命令所產生的新RDB文件不會包含已經過期的鍵。

❑執行BGREWRITEAOF命令所產生的重寫AOF文件不會包含已經過期的鍵。

❑當一個過期鍵被刪除之后,服務器會追加一條DEL命令到現有AOF文件的末尾,顯式地刪除過期鍵。

❑當主服務器刪除一個過期鍵之后,它會向所有從服務器發送一條DEL命令,顯式地刪除過期鍵。

❑從服務器即使發現過期鍵也不會自作主張地刪除它,而是等待主節點發來DEL命令,這種統一、中心化的過期鍵刪除策略可以保證主從服務器數據的一致性。

❑當Redis命令對數據庫進行修改之后,服務器會根據配置向客戶端發送數據庫通知。

 

 

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM