redis源碼5.0.5閱讀整理(未完成)

  文中內容參考《redis設計與實現(第二版)》和redis源碼,由於該書寫的比較早,主要以源碼(redis5.0.5)爲主。

  雖說是參考的源碼的,但是主要內容仍然來自書籍。

  筆記中的內容並不完整,redis有點兒多,先做一個簡單的整理,如果工作中會用到的話再做一個完整的版本吧!
  前面的相關數據結構部分可以看一下,後面的內容並沒有很全面的整理,之後需要的話對源碼進行一次完整的閱讀。

1 redis數據結構與對象

1.1 動態字符串

1.1.1 數據結構定義

//deps/hiredis/sds.h
typedef char *sds;

/* Note: sdshdr5 is never used, we just access the flags byte directly.
 * However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

  不同字段的含義:

  • len:佔4個字節,已使用長度;
  • alloc:佔4個字節,申請到的長度;
  • flags:佔1個字節,低三位表示當前字符串的類型,高位並未使用;
  • buf:數據域,是一個柔型數組。

  並且字符串支持不同長度版本:sds8.sds16,sds32,sds64
在這裏插入圖片描述

1.1.2 相關API

函數 功能 時間複雜度
sdsnew 創建一個包含給定 C 字符串的 SDS 。 O(N) , N 爲給定 C 字符串的長度。
sdsempty 創建一個不包含任何內容的空 SDS 。 O(1)
sdsfree 釋放給定的 SDS 。 O(1)
sdslen 返回 SDS 的已使用空間字節數。 這個值可以通過讀取 SDS 的 len 屬性來直接獲得, 複雜度爲 O(1) 。
sdsavail 返回 SDS 的未使用空間字節數。 這個值可以通過讀取 SDS 的 free 屬性來直接獲得, 複雜度爲 O(1) 。
sdsdup 創建一個給定 SDS 的副本(copy)。 O(N) , N 爲給定 SDS 的長度。
sdsclear 清空 SDS 保存的字符串內容。 因爲惰性空間釋放策略,複雜度爲 O(1) 。
sdscat 將給定 C 字符串拼接到 SDS 字符串的末尾。 O(N) , N 爲被拼接 C 字符串的長度。
sdscatsds 將給定 SDS 字符串拼接到另一個 SDS 字符串的末尾。 O(N) , N 爲被拼接 SDS 字符串的長度。
sdscpy 將給定的 C 字符串複製到 SDS 裏面, 覆蓋 SDS 原有的字符串。 O(N) , N 爲被複制 C 字符串的長度。
sdsgrowzero 用空字符將 SDS 擴展至給定長度。 O(N) , N 爲擴展新增的字節數。
sdsrange 保留 SDS 給定區間內的數據, 不在區間內的數據會被覆蓋或清除。 O(N) , N 爲被保留數據的字節數。
sdstrim 接受一個 SDS 和一個 C 字符串作爲參數, 從 SDS 左右兩端分別移除所有在 C 字符串中出現過的字符。 O(M*N) , M 爲 SDS 的長度, N 爲給定 C 字符串的長度。
sdscmp 對比兩個 SDS 字符串是否相同。 O(N) , N 爲兩個 SDS 中較短的那個 SDS 的長度。

1.1.3 實現原理

  redis中的字符串實現的一個特點是爲字符數組加上了左右邊界。

1.1.3.1 空間預分配

  這一點很想C++中的vector,如果用戶需要n個字節的空間,則redis會分配大於n的空間(一般爲2*n)進行預分配減少之後的空間分配操作,提升性能。
  redis的內存分配策略爲:

  • 如果sds的長度(len值)小於1mb,則得到的空間爲2*len+1,即free=len,一個字符存儲結尾字符'\0'
  • 如果sds的長度(len值)大於等於1mb,則得到的空間爲len+1kb+1
    #define SDS_MAX_PREALLOC (1024*1024)
    //...
    if (newlen < SDS_MAX_PREALLOC)
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC;

1.1.3.2 free-on-write(惰性空間釋放)

  free-on-write這個詞是我按照copy-on-write聯想出來的,其基本原理也類似:即當用戶釋放內存空間時,系統並不會真正的釋放空間而是標記記錄,待到用戶需要寫這部分空間或者系統內存不足時再進行釋放。這樣的好處是如果用戶之後還會對目標數據進行操作擴展就不需要進行額外的申請。
  並未在源碼中找到類似的機制,只有清空字符串時並未進行只是設置部分數據而已,嚴格意義上不能算作是redis針對字符串數據的一種機制。

void sdsfree(sds s) {
    if (s == NULL) return;
    s_free((char*)s-sdsHdrSize(s[-1]));
}

void sdsclear(sds s) {
    sdssetlen(s, 0);
    s[0] = '\0';
}

1.1.3.3 二進制安全

  因爲通過,lenfree確定了字符串的區間,而不是簡單的使用'\0'進行標記,一定程度上保證了該數據不會出現緩衝區溢出。並且能夠保存除了字符串之外的其他類型面向byte數據,如圖像,二進制數據等等。
  但是需要注意的是這並不是嚴格意義上的二進制安全,在stsupdatelenapi中使用了c函數庫提供的strlen進行長度更新,整個redis這個函數基本沒使用過,因此這個函數儘量不要使用。

void sdsupdatelen(sds s) {
    size_t reallen = strlen(s);
    sdssetlen(s, reallen);
}

1.1.3.4 其他技巧

  在redis源碼中可以看到訪問sds都是使用如下方式:

typedef char *sds;
sds s;
//...
unsigned char flags = s[-1];
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
//...

  原因很簡單,整個結構體的頭部大小是固定的:sizeof(int) * 2 + sizeof(char),如果知道了結構體中數據指針的地址很容易推斷到結構體開頭的地址。這樣做的好處是可以方便使用,部分兼容語言的庫函數。
  結構體中的buf[]是一個柔性數組,並不佔用結構體的大小,因此sizeof(struct sdshdr)實際上是sizeof(len)+sizeof(alloc)+sizeof(flags)

結構中最後一個元素允許是未知大小的數組,這個數組就是柔性數組。但結構中的柔性數組前面必須至少一個其他成員,柔性數組成員允許結構中包含一個大小可變的數組。sizeof返回的這種結構大小不包括柔性數組的內存。包含柔數組成員的結構用malloc函數進行內存的動態分配,且分配的內存應該大於結構的大小以適應柔性數組的預期大小。

1.1.4 sds和C字符串的區別

C字符串 SDS
可能出現緩衝區溢出 不會造成緩衝區溢出
只能存儲文本數據 二進制安全,可以存儲任意數據
可以使用<string.h>庫函數 可以部分使用<string.h>庫函數

1.2 鏈表

1.2.1 數據結構定義

  鏈表的結點定義:

typedef struct listNode {
    struct listNode *prev;          //上一個節點
    struct listNode *next;          //下一個節點
    void *value;                    //數據域
} listNode;

  鏈表的迭代器定義:

typedef struct listIter {
    listNode *next;                 //下一個節點
    int direction;                  //方向,AL_START_HEAD或者AL_START_TAIL
} listIter;

  鏈表定義:

typedef struct list {
    listNode *head;                 //鏈表的頭指針
    listNode *tail;                 //鏈表的尾指針
    void *(*dup)(void *ptr);        //複製節點的函數指針
    void (*free)(void *ptr);        //釋放節點的函數指針
    int (*match)(void *ptr, void *key); //節點值對比函數指針
    unsigned int len;               //鏈表的長度
} list;

  從上面的結構體定義中可以看到,redis中的字典實現的基礎數據結構是hash表。
在這裏插入圖片描述

1.2.2 相關API

函數 功能 時間複雜度
listSetDupMethod 將給定的函數設置爲鏈表的節點值複製函數。 O(1) 。
listGetDupMethod 返回鏈表當前正在使用的節點值複製函數。 複製函數可以通過鏈表的 dup 屬性直接獲得, O(1)
listSetFreeMethod 將給定的函數設置爲鏈表的節點值釋放函數。 O(1) 。
listGetFree 返回鏈表當前正在使用的節點值釋放函數。 釋放函數可以通過鏈表的 free 屬性直接獲得, O(1)
listSetMatchMethod 將給定的函數設置爲鏈表的節點值對比函數。 O(1)
listGetMatchMethod 返回鏈表當前正在使用的節點值對比函數。 對比函數可以通過鏈表的 match 屬性直接獲得,O
listLength 返回鏈表的長度(包含了多少個節點)。 鏈表長度可以通過鏈表的 len 屬性直接獲得, O(1) 。
listFirst 返回鏈表的表頭節點。 表頭節點可以通過鏈表的 head 屬性直接獲得, O(1) 。
listLast 返回鏈表的表尾節點。 表尾節點可以通過鏈表的 tail 屬性直接獲得, O(1) 。
listPrevNode 返回給定節點的前置節點。 前置節點可以通過節點的 prev 屬性直接獲得, O(1) 。
listNextNode 返回給定節點的後置節點。 後置節點可以通過節點的 next 屬性直接獲得, O(1) 。
listNodeValue 返回給定節點目前正在保存的值。 節點值可以通過節點的 value 屬性直接獲得, O(1) 。
listCreate 創建一個不包含任何節點的新鏈表。 O(1)
listAddNodeHead 將一個包含給定值的新節點添加到給定鏈表的表頭。 O(1)
listAddNodeTail 將一個包含給定值的新節點添加到給定鏈表的表尾。 O(1)
listInsertNode 將一個包含給定值的新節點添加到給定節點的之前或者之後。 O(1)
listSearchKey 查找並返回鏈表中包含給定值的節點。 O(N) , N 爲鏈表長度。
listIndex 返回鏈表在給定索引上的節點。 O(N) , N 爲鏈表長度。
listDelNode 從鏈表中刪除給定節點。 O(1) 。
listRotate 將鏈表的表尾節點彈出,然後將被彈出的節點插入到鏈表的表頭, 成爲新的表頭節點。 O(1)
listDup 複製一個給定鏈表的副本。 O(N) , N 爲鏈表長度。
listRelease 釋放給定鏈表,以及鏈表中的所有節點。 O(N) , N 爲鏈表長度。

1.2.3 實現原理

  鏈表的實現比較直接,就是簡單的雙向鏈表,通過一個數據結構保存鏈表的頭指針,尾指針,長度,操作函數指針等信息。
  另外實現了一個鏈表的迭代器方便的鏈表的訪問。

1.3 字典

  字典的實現原理是哈希表。

1.3.1 數據結構定義

  hash表定義:

typedef struct dictht {
    dictEntry **table;          //哈希表數組,dictEntry的指針數組
    unsigned long size;         //哈希表的大小
    unsigned long sizemask;     //總是等於size - 1,配合hash值計算對應值的插入位置
    unsigned long used;         //hash表中的節點數量
} dictht;

  hash表節點定義:

typedef struct dictEntry {
    void *key;                  //鍵
    void *val;                  //值
    struct dictEntry *next;     //指向下一個hash表的節點
} dictEntry;

  redis中的字典:

typedef struct dict {
    dictType *type;     //各種類型的操作函數
    void *privdata;     //私有數據
    dictht ht[2];       //哈希表
    int rehashidx;      //用來記錄rehash進度的變量
    int iterators; /* number of iterators currently running */
} dict;
typedef struct dictType {
    unsigned int (*hashFunction)(const void *key);      //hash函數
    void *(*keyDup)(void *privdata, const void *key);   //複製鍵函數
    void *(*valDup)(void *privdata, const void *obj);   //複製值函數
    int (*keyCompare)(void *privdata, const void *key1, const void *key2);  //鍵的比較函數
    void (*keyDestructor)(void *privdata, void *key);   //銷燬鍵函數
    void (*valDestructor)(void *privdata, void *obj);   //銷燬值函數
} dictType;

在這裏插入圖片描述

1.3.2 相關API

函數 功能 時間複雜度
dictCreate 創建一個新的字典。 O(1)
dictAdd 將給定的鍵值對添加到字典裏面。 O(1)
dictReplace 將給定的鍵值對添加到字典裏面, 如果鍵已經存在於字典,那麼用新值取代原有的值。 O(1)
dictFetchValue 返回給定鍵的值。 O(1)
dictGetRandomKey 從字典中隨機返回一個鍵值對。 O(1)
dictDelete 從字典中刪除給定鍵所對應的鍵值對。 O(1)
dictRelease 釋放給定字典,以及字典中包含的所有鍵值對。 O(N) , N 爲字典包含的鍵值對數量。

1.3.3 實現原理

1.3.3.1 hash索引

  redis字典計算hash索引的方式如下,即通過hash算法得到對應key的hash值然後和sizemask與操作就是最終的索引值:


idx = hash & d->ht[table].sizemask;

  redis字典採用的hash算法是SipHash算法。

  大部分非加密哈希算法的改良,都集中在讓哈希速度更快更好上。SipHash 則是個異類,它的提出是爲了解決一類安全問題:hash flooding。通過讓輸出隨機化,SipHash 能夠有效減緩 hash flooding 攻擊。憑藉這一點,它逐漸成爲 Ruby、Python、Rust 等語言默認的 hash 表實現的一部分。

uint64_t siphash(const uint8_t *in, const size_t inlen, const uint8_t *k) {
#ifndef UNALIGNED_LE_CPU
    uint64_t hash;
    uint8_t *out = (uint8_t*) &hash;
#endif
    uint64_t v0 = 0x736f6d6570736575ULL;
    uint64_t v1 = 0x646f72616e646f6dULL;
    uint64_t v2 = 0x6c7967656e657261ULL;
    uint64_t v3 = 0x7465646279746573ULL;
    uint64_t k0 = U8TO64_LE(k);
    uint64_t k1 = U8TO64_LE(k + 8);
    uint64_t m;
    const uint8_t *end = in + inlen - (inlen % sizeof(uint64_t));
    const int left = inlen & 7;
    uint64_t b = ((uint64_t)inlen) << 56;
    v3 ^= k1;
    v2 ^= k0;
    v1 ^= k1;
    v0 ^= k0;

    for (; in != end; in += 8) {
        m = U8TO64_LE(in);
        v3 ^= m;

        SIPROUND;

        v0 ^= m;
    }

    switch (left) {
    case 7: b |= ((uint64_t)in[6]) << 48; /* fall-thru */
    case 6: b |= ((uint64_t)in[5]) << 40; /* fall-thru */
    case 5: b |= ((uint64_t)in[4]) << 32; /* fall-thru */
    case 4: b |= ((uint64_t)in[3]) << 24; /* fall-thru */
    case 3: b |= ((uint64_t)in[2]) << 16; /* fall-thru */
    case 2: b |= ((uint64_t)in[1]) << 8; /* fall-thru */
    case 1: b |= ((uint64_t)in[0]); break;
    case 0: break;
    }

    v3 ^= b;

    SIPROUND;

    v0 ^= b;
    v2 ^= 0xff;

    SIPROUND;
    SIPROUND;

    b = v0 ^ v1 ^ v2 ^ v3;
#ifndef UNALIGNED_LE_CPU
    U64TO8_LE(out, b);
    return hash;
#else
    return b;
#endif
}

uint64_t siphash_nocase(const uint8_t *in, const size_t inlen, const uint8_t *k)
{
#ifndef UNALIGNED_LE_CPU
    uint64_t hash;
    uint8_t *out = (uint8_t*) &hash;
#endif
    uint64_t v0 = 0x736f6d6570736575ULL;
    uint64_t v1 = 0x646f72616e646f6dULL;
    uint64_t v2 = 0x6c7967656e657261ULL;
    uint64_t v3 = 0x7465646279746573ULL;
    uint64_t k0 = U8TO64_LE(k);
    uint64_t k1 = U8TO64_LE(k + 8);
    uint64_t m;
    const uint8_t *end = in + inlen - (inlen % sizeof(uint64_t));
    const int left = inlen & 7;
    uint64_t b = ((uint64_t)inlen) << 56;
    v3 ^= k1;
    v2 ^= k0;
    v1 ^= k1;
    v0 ^= k0;

    for (; in != end; in += 8) {
        m = U8TO64_LE_NOCASE(in);
        v3 ^= m;

        SIPROUND;

        v0 ^= m;
    }

    switch (left) {
    case 7: b |= ((uint64_t)siptlw(in[6])) << 48; /* fall-thru */
    case 6: b |= ((uint64_t)siptlw(in[5])) << 40; /* fall-thru */
    case 5: b |= ((uint64_t)siptlw(in[4])) << 32; /* fall-thru */
    case 4: b |= ((uint64_t)siptlw(in[3])) << 24; /* fall-thru */
    case 3: b |= ((uint64_t)siptlw(in[2])) << 16; /* fall-thru */
    case 2: b |= ((uint64_t)siptlw(in[1])) << 8; /* fall-thru */
    case 1: b |= ((uint64_t)siptlw(in[0])); break;
    case 0: break;
    }

    v3 ^= b;

    SIPROUND;

    v0 ^= b;
    v2 ^= 0xff;

    SIPROUND;
    SIPROUND;

    b = v0 ^ v1 ^ v2 ^ v3;
#ifndef UNALIGNED_LE_CPU
    U64TO8_LE(out, b);
    return hash;
#else
    return b;
#endif
}

  解決衝突:因爲hash表,一定存在不同的key,得到相同的hash值的情況,redis字典解決衝突的方式是使用鏈地址法,每次將新插入的節點插入到對應索引位置的頭部,可以保證插入操作爲O(1)

1.3.3.2 rehash

  隨着字典的不斷使用,其中hash表的負載因子(used/size)會不斷提升或者減小,導致hash表太過緊湊或者鬆散。redis提供rehash機制對hash表進行相應的擴展或者收縮。
  redis哈希表rehash算法步驟

  1. 字典中的ht[1]是輔助進行rehash的表,進行rehash時,首先需要給ht[1]分配空間:
    1. 如果執行的是擴展操作,ht[1]的大小爲第一個大於等於ht[0].used * 22n2^n;
    2. 如果執行的是收縮操作,ht[1]的大小爲第一個大於等於ht[0].used2n2^n;
  2. 將保存在ht[0]上的所有數據進行rehash(重新計算hash索引),保存到h[1]上;
  3. ht[0]上的數據都遷移到ht[1]上時,則釋放ht[0],將ht[1]設置爲ht[0],並創建一個空的表作爲ht[1]

  另外需要注意的是redis判斷rehash完成的依據是ht[0].used==0之後設置rehashidx==-1rehashidx記錄當前rehash到那個表節點。

int dictRehash(dict *d, int n) {
    int empty_visits = n*10; /* Max number of empty buckets to visit. */
    if (!dictIsRehashing(d)) return 0;

    while(n-- && d->ht[0].used != 0) {
        dictEntry *de, *nextde;

        /* Note that rehashidx can't overflow as we are sure there are more
         * elements because ht[0].used != 0 */
        assert(d->ht[0].size > (unsigned long)d->rehashidx);
        while(d->ht[0].table[d->rehashidx] == NULL) {
            d->rehashidx++;
            if (--empty_visits == 0) return 1;
        }
        de = d->ht[0].table[d->rehashidx];
        /* Move all the keys in this bucket from the old to the new hash HT */
        while(de) {
            uint64_t h;

            nextde = de->next;
            /* Get the index in the new hash table */
            h = dictHashKey(d, de->key) & d->ht[1].sizemask;
            de->next = d->ht[1].table[h];
            d->ht[1].table[h] = de;
            d->ht[0].used--;
            d->ht[1].used++;
            de = nextde;
        }
        d->ht[0].table[d->rehashidx] = NULL;
        d->rehashidx++;
    }

    /* Check if we already rehashed the whole table... */
    if (d->ht[0].used == 0) {
        zfree(d->ht[0].table);
        d->ht[0] = d->ht[1];
        _dictReset(&d->ht[1]);
        d->rehashidx = -1;
        return 0;
    }

    /* More to rehash... */
    return 1;
}

  rehash的時機

  1. 服務器目前沒有在執行 BGSAVE 命令或者 BGREWRITEAOF 命令, 並且哈希表的負載因子大於等於 1 (used/size);
  2. 服務器目前正在執行 BGSAVE 命令或者 BGREWRITEAOF 命令, 並且哈希表的負載因子大於等於 5 (used/size);

  當條件1和條件2其中一個被滿足時則開始進行rehash擴展。

根據 BGSAVE 命令或 BGREWRITEAOF 命令是否正在執行, 服務器執行擴展操作所需的負載因子並不相同, 這是因爲在執行 BGSAVE 命令或BGREWRITEAOF 命令的過程中, redis 需要創建當前服務器進程的子進程, 而大多數操作系統都採用寫時複製(copy-on-write)技術來優化子進程的使用效率, 所以在子進程存在期間, 服務器會提高執行擴展操作所需的負載因子, 從而儘可能地避免在子進程存在期間進行哈希表擴展操作, 這可以避免不必要的內存寫入操作, 最大限度地節約內存。

  當哈希表的負載因子小於 0.1 時, 程序自動開始對哈希表執行收縮操作。

  漸進式rehash
  redis如果一次性的對所有數據進行rehash則可能出現服務短時間內無法使用的情況,因此redis採用的是漸進式的rehash,即每次只進行一部分rehash,並且使用rehashidx記錄rehash的進度。
  如下代碼所示,每次只進行指定ms級的rehash,當超過預定時間則中斷。

/* Rehash for an amount of time between ms milliseconds and ms+1 milliseconds */
int dictRehashMilliseconds(dict *d, int ms) {
    long long start = timeInMilliseconds();
    int rehashes = 0;

    while(dictRehash(d,100)) {
        rehashes += 100;
        if (timeInMilliseconds()-start > ms) break;
    }
    return rehashes;
}

1.4 跳躍表

  跳躍表(skiplist)是一種有序數據結構, 它通過在每個節點中維持多個指向其他節點的指針, 從而達到快速訪問節點的目的。
  在大部分情況下, 跳躍表的效率可以和平衡樹相媲美, 並且因爲跳躍表的實現比平衡樹要來得更爲簡單, 所以有不少程序都使用跳躍表來代替平衡樹。redis使用跳躍表作爲有序集合鍵的底層實現之一: 如果一個有序集合包含的元素數量比較多, 又或者有序集合中元素的成員(member)是比較長的字符串時, redis 就會使用跳躍表來作爲有序集合鍵的底層實現。
   redis 只在兩個地方用到了跳躍表, 一個是實現有序集合鍵, 另一個是在集羣節點中用作內部數據結構, 除此之外, 跳躍表在 redis 裏面沒有其他用途。

1.4.1 數據結構定義

  跳躍表的節點:

/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
    robj *obj;                                      //節點所保存的成員對象
    double score;                                   //分值,節點按各自所保存的分值從小到大排列
    struct zskiplistNode *backward;                 //指向當前節點中的前一個節點
    struct zskiplistLevel {
        struct zskiplistNode *forward;              //當前層指向的下一個節點
        unsigned int span;                          //當前層所指向節點和下一個節點之間的距離
    } level[];                                      //層的數組,每個節點可能包含多個層
} zskiplistNode;

  跳躍表管理控制節點:

typedef struct zskiplist {
    struct zskiplistNode *header, *tail;            //分別爲跳躍表的頭結點和尾節點
    unsigned long length;                           //記錄跳躍表的長度,也即是,跳躍表目前包含節點的數量(表頭節點不計算在內)
    int level;                                      //記錄目前跳躍表內,層數最大的那個節點的層數(表頭節點的層數不計算在內)
} zskiplist;

  跳躍表簡單的就理解成一個雙向鏈表的升級版,和雙向鏈表不同的地方便是,每個節點包含多個forward指針而已。
  zset實現:

typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;

在這裏插入圖片描述

1.4.2 相關API

函數 功能 時間複雜度
zslCreate 創建一個新的跳躍表。 O(1)
zslFree 釋放給定跳躍表,以及表中包含的所有節點。 O(N) , N 爲跳躍表的長度。
zslInsert 將包含給定成員和分值的新節點添加到跳躍表中。 平均 O(N) , N 爲跳躍表長度。
zslDelete 刪除跳躍表中包含給定成員和分值的節點。 平均 O(N) , N 爲跳躍表長度。
zslGetRank 返回包含給定成員和分值的節點在跳躍表中的排位。 平均 O(N) , N 爲跳躍表長度。
zslGetElementByRank 返回跳躍表在給定排位上的節點。 平均 O(N) , N 爲跳躍表長度。
zslIsInRange 給定一個分值範圍(range), 比如 0 到 15 , 20 到 28,諸如此類, 如果給定的分值範圍包含在跳 躍表的分值範圍之內, 那麼返回 1 ,否則返回 0 。
zslFirstInRange 給定一個分值範圍, 返回跳躍表中第一個符合這個範圍的節點。 平均 O(N) 。 N 爲跳躍表長度。
zslLastInRange 給定一個分值範圍, 返回跳躍表中最後一個符合這個範圍的節點。 平均 O(N) 。 N 爲跳躍表長度。
zslDeleteRangeByScore 給定一個分值範圍, 刪除跳躍表中所有在這個範圍之內的節點。 O(N) , N 爲被刪除節點數
zslDeleteRangeByRank 給定一個排位範圍, 刪除跳躍表中所有在這個範圍之內的節點。 O(N) , N 爲被刪除節點數

1.4.3 其他

  • 每個跳躍表節點的層高都是 1 至 32 之間的隨機數;
  • 在同一個跳躍表中, 多個節點可以包含相同的分值, 但每個節點的成員對象必須是唯一的;
  • 跳躍表中的節點按照分值大小進行排序, 當分值相同時, 節點按照成員對象的大小進行排序。

1.5 整數集合

  整數集合(intset)是 redis 用於保存整數值的集合抽象數據結構, 它可以保存類型爲 int16_t 、 int32_t 或者 int64_t 的整數值, 並且保證集合中不會出現重複元素。整數集合是集合鍵的底層實現之一。

1.5.1 數據結構定義

typedef struct intset {
    uint32_t encoding;      //編碼方式
    uint32_t length;        //集合包含元素的數量
    int8_t contents[];      //保存元素的數組
} intset;

  雖然 intset 結構將 contents 屬性聲明爲 int8_t 類型的數組, 但實際上 contents 數組並不保存任何 int8_t 類型的值 —— contents 數組的真正類型取決於 encoding 屬性的值。

編碼 類型
INTSET_ENC_INT16 int16_t
INTSET_ENC_INT32 int32_t
INTSET_ENC_INT64 int64_t

在這裏插入圖片描述

1.5.2 相關API

函數 功能 時間複雜度
intsetNew 創建一個新的整數集合。 O(1)
intsetAdd 將給定元素添加到整數集合裏面。 O(N)
intsetRemove 從整數集合中移除給定元素。 O(N)
intsetFind 檢查給定值是否存在於集合。 因爲底層數組有序,查找可以通過二分查找法來進行, 所以複雜度爲 O(\log N) 。
intsetRandom 從整數集合中隨機返回一個元素。 O(1)
intsetGet 取出底層數組在給定索引上的元素。 O(1)
intsetLen 返回整數集合包含的元素個數。 O(1)
intsetBlobLen 返回整數集合佔用的內存字節數。 O(1)

1.5.3 升級

  升級
  每當我們要將一個新元素添加到整數集合裏面, 並且新元素的類型比整數集合現有所有元素的類型都要長時, 整數集合需要先進行升級(upgrade), 然後才能將新元素添加到整數集合裏面。
  升級的基本三個步驟爲:

  1. 根據新元素的類型, 擴展整數集合底層數組的空間大小, 併爲新元素分配空間;
  2. 將底層數組現有的所有元素都轉換成與新元素相同的類型, 並將類型轉換後的元素放置到正確的位上, 而且在放置元素的過程中, 需要繼續維持底層數組的有序性質不變;
  3. 將新元素添加到底層數組裏面。

  降級:整數集合不支持降級操作, 一旦對數組進行了升級, 編碼就會一直保持升級後的狀態。

  升級的優點:

  1. 提升靈活性;
  2. 節約內存。

1.6 壓縮列表

  壓縮列表是 redis 爲了節約內存而開發的, 由一系列特殊編碼的連續內存塊組成的順序型(sequential)數據結構。一個壓縮列表可以包含任意多個節點(entry), 每個節點可以保存一個字節數組或者一個整數值。壓縮列表是一種爲節約內存而開發的順序型數據結構。壓縮列表被用作列表鍵和哈希鍵的底層實現之一。

1.6.1 數據結構定義

在這裏插入圖片描述

  壓縮列表中不同字段的含義:

  • zlbytes:佔4字節,記錄整個列表佔用內存字節數;
  • zltail:佔4字節,記錄 壓縮列表尾節點距離起始位置地址偏移
  • zllen:佔2字節,記錄壓縮列表的節點數量;
    • zllen<UINT16_MAX(65535),該值等於節點數;
    • zllen==UINT16_MAX,需要遍歷整個壓縮列表才能得到節點數;
  • entry:列表節點,不固定;
  • zlen:佔1字節,使用0xff標記壓縮列表的結尾。
/* Return total bytes a ziplist is composed of. */
#define ZIPLIST_BYTES(zl)       (*((uint32_t*)(zl)))

/* Return the offset of the last item inside the ziplist. */
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))

/* Return the length of a ziplist, or UINT16_MAX if the length cannot be
 * determined without scanning the whole ziplist. */
#define ZIPLIST_LENGTH(zl)      (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))

/* The size of a ziplist header: two 32 bit integers for the total
 * bytes count and last item offset. One 16 bit integer for the number
 * of items field. */
#define ZIPLIST_HEADER_SIZE     (sizeof(uint32_t)*2+sizeof(uint16_t))

/* Size of the "end of ziplist" entry. Just one byte. */
#define ZIPLIST_END_SIZE        (sizeof(uint8_t))

/* Return the pointer to the first entry of a ziplist. */
#define ZIPLIST_ENTRY_HEAD(zl)  ((zl)+ZIPLIST_HEADER_SIZE)

/* Return the pointer to the last entry of a ziplist, using the
 * last entry offset inside the ziplist header. */
#define ZIPLIST_ENTRY_TAIL(zl)  ((zl)+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)))

/* Return the pointer to the last byte of a ziplist, which is, the
 * end of ziplist FF entry. */
#define ZIPLIST_ENTRY_END(zl)   ((zl)+intrev32ifbe(ZIPLIST_BYTES(zl))-1)
typedef struct zlentry {
    unsigned int prevrawlensize; /* Bytes used to encode the previous entry len*/
    unsigned int prevrawlen;     /* Previous entry len. */
    unsigned int lensize;        /* Bytes used to encode this entry type/len.
                                    For example strings have a 1, 2 or 5 bytes
                                    header. Integers always use a single byte.*/
    unsigned int len;            /* Bytes used to represent the actual entry.
                                    For strings this is just the string length
                                    while for integers it is 1, 2, 3, 4, 8 or
                                    0 (for 4 bit immediate) depending on the
                                    number range. */
    unsigned int headersize;     /* prevrawlensize + lensize. */
    unsigned char encoding;      /* Set to ZIP_STR_* or ZIP_INT_* depending on
                                    the entry encoding. However for 4 bits
                                    immediate integers this can assume a range
                                    of values and must be range-checked. */
    unsigned char *p;            /* Pointer to the very start of the entry, that
                                    is, this points to prev-entry-len field. */
} zlentry;

在這裏插入圖片描述

  壓縮列表節點不同字段的含義:

  • prevrawlensize:佔4字節,存儲當前節點的前一個節點的長度字段佔用字節長度,可能爲1或者5;
  • prevrawlen:佔4個字節,前一個節點的長度;
    • 佔用字節可以使1或者5字節:
    • 當前一節點的長度小於254字節時,該屬性佔用1字節;
    • 當前一字節的長度大於等於254字節時,該屬性佔用5字節,且第一個字節設置爲0xfe,後四個字節存儲長度;
  • lensize:佔4個字節,存儲encoding字段的長度;
  • len:佔4個字節,表示當前節點數據內容的長度;
  • headersize:佔4個字節,prevrawlensize + lensize
  • encoding:佔1個字節,表示數據類型,可選爲ZIP_STR_,ZIP_INT_
    • 字節數組編碼,1、2或者5字節,值的最高位爲00、01或者10,去除最高位的兩位爲數據的長度;
    • 整數值,1字節,最高位爲11,去除最高位的兩位爲數據的長度;
      • 11000000int16_t;
      • 11010000int32_t;
      • 11100000int64_t;
      • 11110000:24位有符號數;
      • 11111110:8位有符號數;
  • p:數據域。
#define ZIP_STR_MASK 0xc0
#define ZIP_INT_MASK 0x30
#define ZIP_STR_06B (0 << 6)
#define ZIP_STR_14B (1 << 6)
#define ZIP_STR_32B (2 << 6)
#define ZIP_INT_16B (0xc0 | 0<<4)
#define ZIP_INT_32B (0xc0 | 1<<4)
#define ZIP_INT_64B (0xc0 | 2<<4)
#define ZIP_INT_24B (0xc0 | 3<<4)
#define ZIP_INT_8B 0xfe

  解壓Entry

void zipEntry(unsigned char *p, zlentry *e) {

    ZIP_DECODE_PREVLEN(p, e->prevrawlensize, e->prevrawlen);
    ZIP_DECODE_LENGTH(p + e->prevrawlensize, e->encoding, e->lensize, e->len);
    e->headersize = e->prevrawlensize + e->lensize;
    e->p = p;
}
/* Return the length of the previous element, and the number of bytes that
 * are used in order to encode the previous element length.
 * 'ptr' must point to the prevlen prefix of an entry (that encodes the
 * length of the previous entry in order to navigate the elements backward).
 * The length of the previous entry is stored in 'prevlen', the number of
 * bytes needed to encode the previous entry length are stored in
 * 'prevlensize'. */
#define ZIP_DECODE_PREVLEN(ptr, prevlensize, prevlen) do {                     \
    ZIP_DECODE_PREVLENSIZE(ptr, prevlensize);                                  \
    if ((prevlensize) == 1) {                                                  \
        (prevlen) = (ptr)[0];                                                  \
    } else if ((prevlensize) == 5) {                                           \
        assert(sizeof((prevlen)) == 4);                                    \
        memcpy(&(prevlen), ((char*)(ptr)) + 1, 4);                             \
        memrev32ifbe(&prevlen);                                                \
    }                                                                          \
} while(0);
/* Decode the entry encoding type and data length (string length for strings,
 * number of bytes used for the integer for integer entries) encoded in 'ptr'.
 * The 'encoding' variable will hold the entry encoding, the 'lensize'
 * variable will hold the number of bytes required to encode the entry
 * length, and the 'len' variable will hold the entry length. */
#define ZIP_DECODE_LENGTH(ptr, encoding, lensize, len) do {                    \
    ZIP_ENTRY_ENCODING((ptr), (encoding));                                     \
    if ((encoding) < ZIP_STR_MASK) {                                           \
        if ((encoding) == ZIP_STR_06B) {                                       \
            (lensize) = 1;                                                     \
            (len) = (ptr)[0] & 0x3f;                                           \
        } else if ((encoding) == ZIP_STR_14B) {                                \
            (lensize) = 2;                                                     \
            (len) = (((ptr)[0] & 0x3f) << 8) | (ptr)[1];                       \
        } else if ((encoding) == ZIP_STR_32B) {                                \
            (lensize) = 5;                                                     \
            (len) = ((ptr)[1] << 24) |                                         \
                    ((ptr)[2] << 16) |                                         \
                    ((ptr)[3] <<  8) |                                         \
                    ((ptr)[4]);                                                \
        } else {                                                               \
            panic("Invalid string encoding 0x%02X", (encoding));               \
        }                                                                      \
    } else {                                                                   \
        (lensize) = 1;                                                         \
        (len) = zipIntSize(encoding);                                          \
    }                                                                          \
} while(0);

1.6.2 相關API

函數 功能 時間複雜度
ziplistNew 創建一個新的壓縮列表。 O(1)
ziplistPush 創建一個包含給定值的新節點, 並將這個新節點添加到壓縮列表的表頭或者表尾。 平均 O(N^2) 。
ziplistInsert 將包含給定值的新節點插入到給定節點之後。 平均 O(N^2) 。
ziplistIndex 返回壓縮列表給定索引上的節點。 O(N)
ziplistFind 在壓縮列表中查找並返回包含了給定值的節點。 因爲節點的值可能是一個字節數
ziplistNext 返回給定節點的下一個節點。 O(1)
ziplistPrev 返回給定節點的前一個節點。 O(1)
ziplistGet 獲取給定節點所保存的值。 O(1)
ziplistDelete 從壓縮列表中刪除給定的節點。 平均 O(N^2) 。
ziplistDeleteRange 刪除壓縮列表在給定索引上的連續多個節點。 平均 O(N^2) 。
ziplistBlobLen 返回壓縮列表目前佔用的內存字節數。 O(1)
ziplistLen 返回壓縮列表目前包含的節點數量。 節點數量小於 65535 時 O(1) 。

1.6.3 連鎖更新

  之前說過,redis的中的prevrawlen保存的是上一個節點的長度,當節點長度不同時,該字段佔用的字節數也不同,也就是說由於添加數據,刪除數據導致之前的節點長度的變化也會到後續節點的長度。此時需要對多個節點的prevrawlen進行更新,即連鎖更新。
  因爲連鎖更新在最壞情況下需要對壓縮列表執行 N 次空間重分配操作, 而每次空間重分配的最壞複雜度爲 O(N^2) 。

  要注意的是, 儘管連鎖更新的複雜度較高, 但它真正造成性能問題的機率是很低的:

  • 首先, 壓縮列表裏要恰好有多個連續的、長度介於 250 字節至 253 字節之間的節點, 連鎖更新纔有可能被引發, 在實際中, 這種情況並不多見;
  • 其次, 即使出現連鎖更新, 但只要被更新的節點數量不多, 就不會對性能造成任何影響: 比如說, 對三五個節點進行連鎖更新是絕對不會影響性能的。
unsigned char *__ziplistCascadeUpdate(unsigned char *zl, unsigned char *p) {
    size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), rawlen, rawlensize;
    size_t offset, noffset, extra;
    unsigned char *np;
    zlentry cur, next;

    while (p[0] != ZIP_END) {
        zipEntry(p, &cur);
        rawlen = cur.headersize + cur.len;
        rawlensize = zipStorePrevEntryLength(NULL,rawlen);

        /* Abort if there is no next entry. */
        if (p[rawlen] == ZIP_END) break;
        zipEntry(p+rawlen, &next);

        /* Abort when "prevlen" has not changed. */
        if (next.prevrawlen == rawlen) break;

        if (next.prevrawlensize < rawlensize) {
            /* The "prevlen" field of "next" needs more bytes to hold
             * the raw length of "cur". */
            offset = p-zl;
            extra = rawlensize-next.prevrawlensize;
            zl = ziplistResize(zl,curlen+extra);
            p = zl+offset;

            /* Current pointer and offset for next element. */
            np = p+rawlen;
            noffset = np-zl;

            /* Update tail offset when next element is not the tail element. */
            if ((zl+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))) != np) {
                ZIPLIST_TAIL_OFFSET(zl) =
                    intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+extra);
            }

            /* Move the tail to the back. */
            memmove(np+rawlensize,
                np+next.prevrawlensize,
                curlen-noffset-next.prevrawlensize-1);
            zipStorePrevEntryLength(np,rawlen);

            /* Advance the cursor */
            p += rawlen;
            curlen += extra;
        } else {
            if (next.prevrawlensize > rawlensize) {
                /* This would result in shrinking, which we want to avoid.
                 * So, set "rawlen" in the available bytes. */
                zipStorePrevEntryLengthLarge(p+rawlen,rawlen);
            } else {
                zipStorePrevEntryLength(p+rawlen,rawlen);
            }

            /* Stop here, as the raw length of "next" has not changed. */
            break;
        }
    }
    return zl;
}

1.7 壓縮字典

  zipmap利用字符串實現了一個簡單的hash_table結構,又通過固定的字節表示節省空間。zipmap和前面介紹的ziplist結構十分類似。

1.7.1 數據結構定義

在這裏插入圖片描述

  各個字段的含義:

  • zmlen:佔1個字節,表示當前鍵值對的數量;
    • zmlen<254時,表示鍵值對數量;
    • zmlen>=254時,只能通過遍歷確定大小
  • 鍵值對:
    • key_len:編碼類似ziplist,可以是1個或者5個字節:
      • 1個字節,key的長度小於254;
      • 5個字節,key的長度大於等於254;
    • key:鍵;
    • value_len:編碼類似ziplist,可以是1個或者5個字節:
      • 1個字節,value的長度小於254;
      • 5個字節,value的長度大於等於254;
    • free:佔1個字節,表示value後的空閒長度;
    • value:值;
  • end:佔用1個字節,借位字符,值爲0xff

1.7.2 相關API

函數 功能 時間複雜度
zipmapNew 創建空的zipmap O(1)
zipmapLookupRaw 查找目標鍵對應的value O(n)
zipmapSet 插入鍵值對,如果存在就更新 O(n)
zipmapExists 判斷鍵是否存在 O(n)

1.8 快速鏈表

  quicklist是redis3.2中引入的新結構,能夠在時間效率和空間效率間實現較好的折中。quicklist是一個雙向鏈表,鏈表中的每個節點都是一個ziplist結構,quicklist可以看成是將雙向鏈表將若干個小型的ziplist組合在一起的數據結構。當ziplist節點個數較多的時候,quicklist退化成雙向鏈表,一個極端的情況就是每個ziplist節點只有一個entry,即只有一個元素。當ziplist元素較少的時候,quicklist可以退化成ziplist,另一種極端的情況就是,整個quicklist中只有一個ziplist節點。

1.8.1 數據結構定義

在這裏插入圖片描述

  

typedef struct quicklist {
    quicklistNode *head;
    quicklistNode *tail;
    unsigned long count;        /* total count of all entries in all ziplists */
    unsigned long len;          /* number of quicklistNodes */
    int fill : 16;              /* fill factor for individual nodes */
    unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
} quicklist;

  各個字段的含義:

  • head:佔一個機器長度,指向頭結點;
  • tail:佔一個機器長度,指向尾節點;
  • count:佔4個字節,壓縮列表中的entry的數量;
  • count:佔4個字節,節點數量;
  • fill:佔16位,
    • 正數,表示每個ziplist最多包含的數據項數;
    • 負數:
      • -1,ziplist節點最大爲4kb;
      • -2,ziplist節點最大爲8kb;
      • -3,ziplist節點最大爲16kb;
      • -4,ziplist節點最大爲32kb;
      • -5,ziplist節點最大爲64kb;
  • compress:佔16位,快速列表末尾不進行壓縮的節點數。
/* quicklistNode is a 32 byte struct describing a ziplist for a quicklist.
 * We use bit fields keep the quicklistNode at 32 bytes.
 * count: 16 bits, max 65536 (max zl bytes is 65k, so max count actually < 32k).
 * encoding: 2 bits, RAW=1, LZF=2.
 * container: 2 bits, NONE=1, ZIPLIST=2.
 * recompress: 1 bit, bool, true if node is temporarry decompressed for usage.
 * attempted_compress: 1 bit, boolean, used for verifying during testing.
 * extra: 10 bits, free for future use; pads out the remainder of 32 bits */
typedef struct quicklistNode {
    struct quicklistNode *prev;
    struct quicklistNode *next;
    unsigned char *zl;
    unsigned int sz;             /* ziplist size in bytes */
    unsigned int count : 16;     /* count of items in ziplist */
    unsigned int encoding : 2;   /* RAW==1 or LZF==2 */
    unsigned int container : 2;  /* NONE==1 or ZIPLIST==2 */
    unsigned int recompress : 1; /* was this node previous compressed? */
    unsigned int attempted_compress : 1; /* node can't compress; too small */
    unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;

  各個字段的含義:

  • prev:上一個節點的指針;
  • next:下一個節點的指針
  • zl:指向元素;
  • sz:佔4個字節,ziplist的大小
  • count:佔16bit,ziplist中的entry數量;
  • encoding:佔2bit,編碼方式
    • RAW:原生編碼;
    • LZF:LZF壓縮編碼;
  • container:佔2bit,zl指向的容器類型:
    • NONE:none;
    • ZIPLIST:ziplist;
  • recompress:1bit,當前節點是否進過壓縮;
  • attempted_compress:1bit,測試時使用;
  • extra:10bit,預留字段

  LZF壓縮格式:

typedef struct quicklistLZF {
    unsigned int sz; /* LZF size in bytes*/
    char compressed[];
} quicklistLZF;

  不同字段的含義:

  • sz:當前字段的長度;
  • compressed:壓縮後的數據。
typedef struct quicklistEntry {
    const quicklist *quicklist;
    quicklistNode *node;
    unsigned char *zi;
    unsigned char *value;
    long long longval;
    unsigned int sz;
    int offset;
} quicklistEntry;

  壓縮列表中的元素結構,各個字段的含義:

  • quicklist
  • node:指向當前元素所在的node;
  • zi:指向當前元素所在的ziplist;
  • value:當前節點的字符串內容;
  • longval:當前節點的數值內容;
  • sz:節點大小;
  • offset:大年節點相對於ziplist的偏移量,即當前節點是第幾個entry

1.8.2 常用API

函數名稱 功能 時間複雜度
quicklistCreate 創建默認quicklist O(1)
quicklistNew 創建自定義屬性quicklist O(1)
quicklistPushHead 在頭部插入數據 O(m)
quicklistPushTail 在尾部插入數據 O(m)
quicklistPush 在頭部或者尾部插入數據 O(m)
quicklistInsertAfter 在某個元素後面插入數據 O(m)
quicklistInsertBefore 在某個元素前面插入數據 O(m)
quicklistDelEntry 刪除某個元素 O(m)
quicklistDelRange 刪除某個區間的所有元素 O(1/m+ m)
quicklistPop 彈出頭部或者尾部元素 O(m)
quicklistReplaceAtIndex 替換某個元素 O(m)
quicklistIndex 獲取某個位置的元素 O(n+m)
quicklistGetIterator 獲取指向頭部或尾部的迭代器 O(1)
quicklistGetIteratorAtIdx 獲取特定位置的迭代器 O(n+m)
quicklistNext 獲取迭代器下一個元素 O(m)

1.8.3 數據壓縮

  壓縮:
  quicklist每個節點的實際數據存儲結構爲ziplist,這種結構的主要優勢在於節省內存空間。爲了進一步降低ziplist佔用空間,redis允許對ziplist再進行一次壓縮,redis採用的壓縮算法是LZF,壓縮後數據可以分爲多個片段,每個片段可以分爲解釋字段和數據字段:

  • 解釋字段:佔用1~3個字節;
  • 數據字段:可能不存在。

在這裏插入圖片描述

  LZF數據壓縮的基本思想是:數據與前面重複的,記錄重複位置以及重複長度,否則直接記錄原始數據內容。基本步驟如下:

  1. 遍歷輸入字符串,對當前字符及其後面2個字符進行散列運算;
  2. 如果在Hash表中找到曾出現的記錄,則計算重複字節的長度以及位置,反之直接輸出數據。

  LZF壓縮的數據格式有三種:

  • 字面型:解釋字段佔用1個字節,數據字段的長度等於一個字節的低5位LLLLL內容+1;
  • 簡短重複型:解釋字段佔用2個字節,沒有數據字段,數據內容與前面的數據重複,重複長度小於8。重複長度等於兩個字節中第一個字節的高三位LLL組成的字面值+2,重複開始偏移量等於第一個字節的低5位和第二個字節組成的數+1;
  • 批量重複型:解釋字段佔用3個字節,沒有數據字段,數據與前面的內容重複。長度是第二個字節LLLL LLLL組成的字面量+9,偏移量爲所有u組成的字面量+1(有疑問:TODO:

  解壓縮:
  根據LZF壓縮後的數據格式,我們可以較爲容易地實現LZF的解壓縮。值得注意的是,可能存在重複數據與當前位置重疊的情況,例如在當前位置前的15個字節處,重複了20個字節,此時需要按位逐個複製。

1.9 radix樹

  radix Tree(基數樹) 事實上就幾乎相同是傳統的二叉樹。僅僅是在尋找方式上。利用比方一個unsigned int的類型的每個比特位作爲樹節點的推斷。redis實現了不定長壓縮前綴的radix tree,用在集羣模式下存儲slot對應的的所有key信息。

1.9.1 數據結構定義

在這裏插入圖片描述

#define RAX_NODE_MAX_SIZE ((1<<29)-1)
typedef struct raxNode {
    uint32_t iskey:1;     /* Does this node contain a key? */
    uint32_t isnull:1;    /* Associated value is NULL (don't store it). */
    uint32_t iscompr:1;   /* Node is compressed. */
    uint32_t size:29;     /* Number of children, or compressed string len. */
    unsigned char data[];
} raxNode;

  各個元素的含義:

  • iskey:這個節點是夠包含key
    • 0:沒有key
    • 1:表示從頭部到其父節點的路徑完整的存儲了key,查找的時候按子節點iskey=1來判斷key是否存在;
  • isnull:是否有存儲value值;
  • iscompr:是否有前綴壓縮,決定了data存儲的數據結構;
    • 0:有size個字符,size個子節點;
    • 1:只有一個子節點
  • size:孩子個數或者該節點存儲的字符個數;
  • data:存儲子節點的信息。
typedef struct rax {
    raxNode *head;      //頭結點指針
    uint64_t numele;    //元素數量
    uint64_t numnodes;  //節點數量
} rax;

  TODO:

1.10 對象

  redis 並沒有直接使用之前提到的數據結構來實現鍵值對數據庫, 而是基於這些數據結構創建了一個對象系統, 這個系統包含字符串對象、列表對象、哈希對象、集合對象和有序集合對象這五種類型的對象, 每種對象都用到了至少一種前面所介紹的數據結構。

1.10.1 不同的對象類型

1.10.1.1 基本的對象結構

#define LRU_BITS 24
typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;
    void *ptr;
} robj;
/* The actual redis Object */
#define OBJ_STRING 0    /* String object. */
#define OBJ_LIST 1      /* List object. */
#define OBJ_SET 2       /* Set object. */
#define OBJ_ZSET 3      /* Sorted set object. */
#define OBJ_HASH 4      /* Hash object. */

#define OBJ_MODULE 5    /* Module object. */
#define OBJ_STREAM 6    /* Stream object. */
/* Objects encoding. Some kind of objects like Strings and Hashes can be
 * internally represented in multiple ways. The 'encoding' field of the object
 * is set to one of this fields for this object. */
#define OBJ_ENCODING_RAW 0     /* Raw representation */
#define OBJ_ENCODING_INT 1     /* Encoded as integer */
#define OBJ_ENCODING_HT 2      /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3  /* Encoded as zipmap */
#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define OBJ_ENCODING_INTSET 6  /* Encoded as intset */
#define OBJ_ENCODING_SKIPLIST 7  /* Encoded as skiplist */
#define OBJ_ENCODING_EMBSTR 8  /* Embedded sds string encoding */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */

  不同字段的含義:

  • type:佔4個字節,表明對象的類型:
    • OBJ_STRING:字符串對象;
    • OBJ_LIST:列表對象;
    • OBJ_SET:集合對象;
    • OBJ_ZSET:有序集合對象;
    • OBJ_HASH:哈希對象;
  • encoding:佔4個字節,表明對象所使用的數據結構,即底層實現:
    • OBJ_ENCODING_RAW:動態字符串;
    • OBJ_ENCODING_INT:整數;
    • OBJ_ENCODING_HT:哈希表;
    • OBJ_ENCODING_ZIPMAP:壓縮字典
    • OBJ_ENCODING_LINKEDLIST:雙向鏈表;
    • OBJ_ENCODING_ZIPLIST:壓縮表;
    • OBJ_ENCODING_INTSET:整數集合;
    • OBJ_ENCODING_SKIPLIST:跳躍表;
    • OBJ_ENCODING_EMBSTR:embstr 編碼的簡單動態字符串;
    • OBJ_ENCODING_QUICKLIST:快速鏈表;
    • OBJ_ENCODING_STREAM:流,使用基數樹實現
  • lru:佔22bit,記錄最後一次被命令程序訪問的時間;
  • refcount:佔4個字節,引用計數;
  • ptr:數據域,指向低層實現數據結構

  通過 encoding 屬性來設定對象所使用的編碼, 而不是爲特定類型的對象關聯一種固定的編碼, 極大地提升了 redis 的靈活性和效率, 因爲 redis 可以根據不同的使用場景來爲一個對象設置不同的編碼, 從而優化對象在某一場景下的效率。

1.10.1.2 字符串對象

1.10.1.2.1 實現

  從上面可以看到字符串對象可選編碼分別爲OBJ_ENCODING_RAWOBJ_ENCODING_INTOBJ_ENCODING_EMBSTR分別對應int,raw,embstr

  • int:如果字符串爲整數值,並且這個整數可以通過long類型表示,直接將數據域void*轉換成long,並把字符串對象的編碼設置爲OBJ_ENCODING_INT
  • raw:如果給定的字符串長度大於39字節,則使用簡單字符串(SDS)保存,編碼設置爲OBJ_ENCODING_RAW
  • emstr:如果字符串的長度小於等於39字節,則使用embstr編碼進行保存,並且編碼設置爲OBJ_ENCODING_EMBSTR

  rawembstr本質上是一樣的,不相同的地方是:embstr是對raw的優化,從結構上來說完全相同,但是實現上embstr第一次申請內存時,直接申請sizeof(redis_object)+sizeof(sdshdr)大小的內存,再進行重新解釋。
在這裏插入圖片描述
在這裏插入圖片描述

  這樣做的優點有:

  1. embstr申請和釋放空間只需要調用一次malloc或者free,而raw需要兩次;
  2. embstr的內存是一塊連續的內存對緩存更加友好。

  需要注意的是redis中long double是直接使用字符串存儲的。

  int,embstr編碼在滿足相關條件下會被轉換成raw編碼。

  redis沒有爲embstr提供相應的修改程序,實際上embstr是隻讀的,如果需要修改redis內部會先將embstr轉換成raw。因此對embstr進行修改之後,編碼就變成raw

  字符串對象是 redis 五種類型的對象中唯一一種會被其他四種類型對象嵌套的對象。

1.10.1.2.2 命令實現
命令 int 編碼的實現方法 embstr 編碼的實現方法 raw 編碼的實現方法
SET 使用 int 編碼保存值。 使用 embstr 編碼保存值。 使用 raw 編碼保存值。
GET 拷貝對象所保存的整數值, 將這個拷貝轉換成字符串值, 然後向客戶端返回這個字符串值。 直接向客戶端返回字符串值。 直接向客戶端返回字符串值。
APPEND 將對象轉換成 raw 編碼, 然後按raw 編碼的方式執行此操作。 將對象轉換成 raw 編 碼, 然後按raw 編碼的方式執行此操作。 調用 sdscatlen 函數, 將給定字符串追加到現有字符串的末尾。
INCRBYFLOAT 取出整數值並將其轉換成 longdouble 類型的浮點數, 對這個浮點數進行加法計算, 然後將得出的浮點數結果保存起來。 取出字符串值並嘗試將其轉換成long double 類型的浮點數, 對這個浮點數進行加法計算, 然後將得出的浮點數結果保存起來。 如果字符串值不能被轉換成浮點數, 那麼向客戶端返回一個錯誤。 取出字符串值並嘗試將其轉換成 longdouble 類型的浮點數, 對這個浮點數進行加法計算, 然後將得出的浮點數結果保存起來。 如果字符串值不能被轉換成浮點數, 那麼向客戶端返回一個錯誤。
INCRBY 對整數值進行加法計算, 得出的計算結果會作爲整數被保存起來。 embstr 編碼不能執行此 命令, 向客戶端返回一個錯誤。
DECRBY 對整數值進行減法計算, 得出的計算結果會作爲整數被保存起來。 embstr 編碼不能執行此 命令, 向客戶端返回一個錯誤。 raw 編碼不能執行此命令, 向客戶端返回一個錯誤。
STRLEN 拷貝對象所保存的整數值, 將這個拷貝轉換成字符串值, 計算並返回這個字符串值的長度。 調用 sdslen 函數, 返回字符串的長度。 調用 sdslen 函數, 返回字符串的長度。
SETRANGE 將對象轉換成 raw 編碼, 然後按raw 編碼的方式執行此命令。 將對象轉換成 raw 編碼, 然後按raw 編碼的方式執行此命令。
GETRANGE 拷貝對象所保存的整數值, 將這個拷貝轉換成字符串值, 然後取出並返回字符串指定索引上的字符。 直接取出並返回字符串指定索引上的字符。 直接取出並返回字符串指定索引上的字符。

1.10.1.3 列表對象

1.10.1.3.1實現原理

  列表對象的編碼可以使ziplist,linkedlist
  ziplist作爲低層實現時,每個壓縮列表節點(entry)保存了一個列表元素。
  linkedlist編碼的列表對象使用雙端鏈表作爲底層實現, 每個雙端鏈表節點(node)都保存了一個字符串對象, 而每個字符串對象都保存了一個列表元素。

  編碼轉換:
  當列表對象滿足如下兩個條件時,列表對象使用ziplist

  • 列表對象保存的所有字符串元素的長度都小於 64 字節;
  • 列表對象保存的元素數量小於 512 個。

  否則,列表對象使用linkedlist編碼。

  以上兩個條件的上限值是可以修改的, 具體請看配置文件中關於 list-max-ziplist-value 選項和 list-max-ziplist-entries 選項的說明。

1.10.1.3.2 命令實現
命令 ziplist 編碼的實現方法 linkedlist 編碼的實現方法
LPUSH 調用 ziplistPush 函數, 將新元素推入到壓縮列表的表頭。 調用 listAddNodeHead 函數, 將新元素推入到雙端鏈表的表頭。
RPUSH 調用 ziplistPush 函數, 將新元素推入到壓縮列表的表尾。 調用 listAddNodeTail 函數, 將新元素推入到雙端鏈表的表尾。
LPOP 調用 ziplistIndex 函數定位壓縮列表的表頭節點, 在向用戶返回節點所保存的元素之後, 調用ziplistDelete 函數刪除表頭節點。 調用 listFirst 函數定位雙端鏈表的表頭節點, 在向用戶返回節點所保存的元素之後, 調用 listDelNode 函數刪除表頭節點。
RPOP 調用 ziplistIndex 函數定位壓縮列表的表尾節點, 在向用戶返回節點所保存的元素之後, 調用ziplistDelete 函數刪除表尾節點。 調用 listLast 函數定位雙端鏈表的表尾節點, 在向用戶返回節點所保存的元素之後, 調用 listDelNode 函數刪除表尾節點。
LINDEX 調用 ziplistIndex 函數定位壓縮列表中的指定節點, 然後返回節點所保存的元素。 調用 listIndex 函數定位雙端鏈表中的指定節點, 然後返回節點所保存的元素。
LLEN 調用 ziplistLen 函數返回壓縮列表的長度。 調用 listLength 函數返回雙端鏈表的長度。
LINSERT 插入新節點到壓縮列表的表頭或者表尾時, 使用ziplistPush 函數; 插入新節點到壓縮列表的其他位置時, 使用 ziplistInsert 函數。 調用 listInsertNode 函數, 將新節點插入到雙端鏈表的指定位置。
LREM 遍歷壓縮列表節點, 並調用 ziplistDelete 函數刪除包含了給定元素的節點。 遍歷雙端鏈表節點, 並調用 listDelNode 函數刪除包含了給定元素的節點。
LTRIM 調用 ziplistDeleteRange 函數, 刪除壓縮列表中所有不在指定索引範圍內的節點。 遍歷雙端鏈表節點, 並調用 listDelNode 函數刪除鏈表中所有不在指定索引範圍內的節點。
LSET 調用 ziplistDelete 函數, 先刪除壓縮列表指定索引上的現有節點, 然後調用 ziplistInsert 函數, 將一個包含給定元素的新節點插入到相同索引上面。 調用 listIndex 函數, 定位到雙端鏈表指定索引上的節點, 然後通過賦值操作更新節點的值。

1.10.1.4 哈希對象

1.10.1.4.1 實現原理

  哈希對象的編碼可以是 ziplist或者 hashtable即字典 。
  ziplist編碼的哈希對象使用壓縮列表作爲底層實現, 每當有新的鍵值對要加入到哈希對象時, 程序會先將保存了鍵的壓縮列表節點推入到壓縮列表表尾, 然後再將保存了值的壓縮列表節點推入到壓縮列表表尾, 因此:

  • 保存了同一鍵值對的兩個節點總是緊挨在一起, 保存鍵的節點在前, 保存值的節點在後;
  • 先添加到哈希對象中的鍵值對會被放在壓縮列表的表頭方向, 而後來添加到哈希對象中的鍵值對會被放在壓縮列表的表尾方向。

  hashtable編碼的哈希對象使用字典作爲底層實現, 哈希對象中的每個鍵值對都使用一個字典鍵值對來保存:

  • 字典的每個鍵都是一個字符串對象, 對象中保存了鍵值對的鍵;
  • 字典的每個值都是一個字符串對象, 對象中保存了鍵值對的值。

  編碼轉換:
  當哈希對象可以同時滿足以下兩個條件時, 哈希對象使用 ziplist編碼:

  1. 哈希對象保存的所有鍵值對的鍵和值的字符串長度都小於 64 字節;
  2. 哈希對象保存的鍵值對數量小於 512 個。
      否則使用 hashtable編碼。

  這兩個條件的上限值是可以修改的, 具體請看配置文件中關於 hash-max-ziplist-value選項和 hash-max-ziplist-entries選項的說明。

1.10.1.4.2 命令實現
命令 ziplist 編碼實現方法 hashtable 編碼的實現方法
HSET 首先調用 ziplistPush 函數, 將鍵推入到壓縮列表的表尾, 然後再次調用 ziplistPush 函數, 將值推入到壓縮列表的表尾。 調用 dictAdd 函數, 將新節點添加到字典裏面。
HGET 首先調用 ziplistFind 函數, 在壓縮列表中查找指定鍵所對應的節點, 然後調用 ziplistNext 函數, 將指針移動到鍵節點旁邊的值節點, 最後返回值節點。 調用 dictFind 函數, 在字典中查找給定鍵, 然後調用dictGetVal 函數, 返回該鍵所對應的值。
HEXISTS 調用 ziplistFind 函數, 在壓縮列表中查找指定鍵所對應的節點, 如果找到的話說明鍵值對存在, 沒找到的話就說明鍵值對不存在。 調用 dictFind 函數, 在字典中查找給定鍵, 如果找到的話說明鍵值對存在, 沒找到的話就說明鍵值對不存在。
HDEL 調用 ziplistFind 函數, 在壓縮列表中查找指定鍵所對應的節點, 然後將相應的鍵節點、 以及鍵節點旁邊的值節點都刪除掉。 調用 dictDelete 函數, 將指定鍵所對應的鍵值對從字典中刪除掉。
HLEN 調用 ziplistLen 函數, 取得壓縮列表包含節點的總數量, 將這個數量除以 2 , 得出的結果就是壓縮列表保存的鍵值對的數量。 調用 dictSize 函數, 返回字典包含的鍵值對數量, 這個數量就是哈希對象包含的鍵值對數量。
HGETALL 遍歷整個壓縮列表, 用 ziplistGet 函數返回所有鍵和值(都是節點)。 遍歷整個字典, 用 dictGetKey 函數返回字典的鍵, 用dictGetVal 函數返回字典的值。

1.10.1.5 集合對象

1.10.1.5.1 實現原理

  集合對象的編碼可以是intset或者 hashtable
  intset編碼的集合對象使用整數集合作爲底層實現, 集合對象包含的所有元素都被保存在整數集合裏面。
   hashtable編碼的集合對象使用字典作爲底層實現, 字典的每個鍵都是一個字符串對象, 每個字符串對象包含了一個集合元素, 而字典的值則全部被設置爲 NULL
  編碼轉換
  當集合對象可以同時滿足以下兩個條件時, 對象使用 intset編碼:

  1. 集合對象保存的所有元素都是整數值;
  2. 集合對象保存的元素數量不超過 512 個。

  不能滿足這兩個條件的集合對象需要使用 hashtable編碼。

  第二個條件的上限值是可以修改的, 具體請看配置文件中關於 set-max-intset-entries 選項的說明。

1.10.1.5.2 命令實現
命令 intset 編碼的實現方法 hashtable 編碼的實現方法
SADD 調用 intsetAdd 函數, 將所有新元素添加到整數集合裏面。 調用 dictAdd , 以新元素爲鍵, NULL 爲值, 將鍵值對添加到字典裏面。
SCARD 調用 intsetLen 函數, 返回整數集合所包含的元素數量, 這個數量就是集合對象所包含的元素數 量。 調用 dictSize 函數, 返回字典所包含的鍵值對數量, 這個數量就是集合對象所包含的元素數量。
SISMEMBER 調用 intsetFind 函數, 在整數集合中查找給定的元素, 如果找到了說明元素存在於集合, 沒找到則說明元素不存在於集合。 調用 dictFind 函數, 在字典的鍵中查找給定的元素, 如果找到了說明元素存在於集合, 沒找到則說明元素不存在於集合。
SMEMBERS 遍歷整個整數集合, 使用 intsetGet 函數返回集合元素。 遍歷整個字典, 使用 dictGetKey 函數返回字典的鍵作爲集合元素。
SRANDMEMBER 調用 intsetRandom 函數, 從整數集合中隨機返回一個元素。 調用 dictGetRandomKey 函數, 從字典中隨機返回一個字典鍵。
SPOP 調用 intsetRandom 函數, 從整數集合中隨機取出一個元素, 在將這個隨機元素返回給客戶端之後, 調用 intsetRemove 函數, 將隨機元素從整數集合中刪除掉。 調用 dictGetRandomKey 函數, 從字典中隨機取出一個字典鍵, 在將這個隨機字典鍵的值返回給客戶端之後, 調用dictDelete 函數, 從字典中刪除隨機字典鍵所對應的鍵值對。
SREM 調用 intsetRemove 函數, 從整數集合中刪除所有給定的元素。 調用 dictDelete 函數, 從字典中刪除所有鍵爲給定元素的鍵值對。

1.10.1.6 有序集合對象

1.10.1.6.1 實現原理

  有序集合的編碼可以是 ziplist或者 skiplist
  ziplist編碼的有序集合對象使用壓縮列表作爲底層實現, 每個集合元素使用兩個緊挨在一起的壓縮列表節點來保存, 第一個節點保存元素的成員(member), 而第二個元素則保存元素的分值(score)。

  壓縮列表內的集合元素按分值從小到大進行排序, 分值較小的元素被放置在靠近表頭的方向, 而分值較大的元素則被放置在靠近表尾的方向。

  skiplist編碼的有序集合對象使用 zset結構作爲底層實現, 一個 zset結構同時包含一個字典和一個跳躍表:

typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;

  其中zsl跳躍表按分值從小到大保存了所有集合元素, 每個跳躍表節點都保存了一個集合元素: 跳躍表節點的object屬性保存了元素的成員, 而跳躍表節點的score屬性則保存了元素的分值。 通過這個跳躍表, 程序可以對有序集合進行範圍型操作。
  其中dict字典爲有序集合創建了一個從成員到分值的映射, 字典中的每個鍵值對都保存了一個集合元素: 字典的鍵保存了元素的成員, 而字典的值則保存了元素的分值。 通過這個字典, 程序可以用 O(1)複雜度查找給定成員的分值。

  有序集合每個元素的成員都是一個字符串對象, 而每個元素的分值都是一個 double類型的浮點數。雖然zset同時使用了跳躍表和字典,但是兩個結構是共享數據成員的,因此不會有重複成員或者分值,造成內存浪費。

  當有序集合對象可以同時滿足以下兩個條件時, 對象使用 ziplist編碼:

  1. 有序集合保存的元素數量小於 128 個;
  2. 有序集合保存的所有元素成員的長度都小於 64 字節。

  否則有序集合對象將使用 skiplist編碼。

  以上兩個條件的上限值是可以修改的, 具體請看配置文件中關於 zset-max-ziplist-entries選項和 zset-max-ziplist-value選項的說明。

1.10.1.6.2 命令實現
命令 ziplist 編碼的實現方法 zset 編碼的實現方法
ZADD 調用 ziplistInsert 函數, 將成員和分值作爲兩個節點分別插入到壓縮列表。 先調用 zslInsert 函數, 將新元素添加到跳躍表, 然後調用 dictAdd 函數, 將新元素關聯到字典。
ZCARD 調用 ziplistLen 函數, 獲得壓縮列表包含節點的數量, 將這個數量除以 2 得出集合元素的數量。 訪問跳躍表數據結構的 length 屬性, 直接返回集合元素的數量。
ZCOUNT 遍歷壓縮列表, 統計分值在給定範圍內的節點的數量。 遍歷跳躍表, 統計分值在給定範圍內的節點的數量。
ZRANGE 從表頭向表尾遍歷壓縮列表, 返回給定索引範圍內的所有元素。 從表頭向表尾遍歷跳躍表, 返回給定索引範圍內的所有元素。
ZREVRANGE 從表尾向表頭遍歷壓縮列表, 返回給定索引範圍內的所有元素。 從表尾向表頭遍歷跳躍表, 返回給定索引範圍內的所有元素。
ZRANK 從表頭向表尾遍歷壓縮列表, 查找給定的成員, 沿途記錄經過節點的數量, 當找到給定成員之後, 途經節點的數量就是該成員所對應元素的排名。 從表頭向表尾遍歷跳躍表, 查找給定的成員, 沿途記錄經過節點的數量, 當找到給定成員之後, 途經節點的數量就是該成員所對應元素的排名。
ZREVRANK 從表尾向表頭遍歷壓縮列表, 查找給定的成員, 沿途記錄經過節點的數量, 當找到給定成員之後, 途經節點的數量就是該成員所對應元素的排名。 從表尾向表頭遍歷跳躍表, 查找給定的成員, 沿途記錄經過節點的數量, 當找到給定成員之後, 途經節點的數量就是該成員所對應元素的排名。
ZREM 遍歷壓縮列表, 刪除所有包含給定成員的節點, 以及被刪除成員節點旁邊的分值節點。 遍歷跳躍表, 刪除所有包含了給定成員的跳躍表節點。 並在字典中解除被刪除元素的成員和分值的關聯。
ZSCORE 遍歷壓縮列表, 查找包含了給定成員的節點, 然後取出成員節點旁邊的分值節點保存的元素分值。 直接從字典中取出給定成員的分值。

1.10.2 對象的使用和管理

1.10.2.1 類型檢查和命令多態

  redis 中用於操作鍵的命令基本上可以分爲兩種類型。

  • 可以對任何類型的鍵執行, 比如說 DEL命令、 EXPIRE命令、 RENAME命令、 TYPE命令、 OBJECT命令, 等等。
  • 只能對特定類型的鍵執行, 比如說:
    • SETGETAPPENDSTRLEN等命令只能對字符串鍵執行;
    • HDELHSETHGETHLEN等命令只能對哈希鍵執行;
    • RPUSHLPOPLINSERTLLEN等命令只能對列表鍵執行;
    • SADDSPOPSINTERSCARD等命令只能對集合鍵執行;
    • ZADDZCARDZRANKZSCORE等命令只能對有序集合鍵執行。

  類型特定命令所進行的類型檢查是通過 redisObject 結構的 type 屬性來實現的:

  • 在執行一個類型特定命令之前, 服務器會先檢查輸入數據庫鍵的值對象是否爲執行命令所需的類型, 如果是的話, 服務器就對鍵執行指定的命令;
  • 否則, 服務器將拒絕執行命令, 並向客戶端返回一個類型錯誤。

  多態命令的實現:
  redis 除了會根據值對象的類型來判斷鍵是否能夠執行指定命令之外, 還會根據值對象的編碼方式, 選擇正確的命令實現代碼來執行命令。本質上是檢查編碼和類型,比如下面部分代碼:

if (o->type == OBJ_STRING) {
    if(o->encoding == OBJ_ENCODING_INT) {
        asize = sizeof(*o);
    } else if(o->encoding == OBJ_ENCODING_RAW) {
        asize = sdsAllocSize(o->ptr)+sizeof(*o);
    } else if(o->encoding == OBJ_ENCODING_EMBSTR) {
        asize = sdslen(o->ptr)+2+sizeof(*o);
    } else {
        serverPanic("Unknown string encoding");
    }
} else if (o->type == OBJ_LIST) {
    if (o->encoding == OBJ_ENCODING_QUICKLIST) {
        quicklist *ql = o->ptr;
        quicklistNode *node = ql->head;
        asize = sizeof(*o)+sizeof(quicklist);
        do {
            elesize += sizeof(quicklistNode)+ziplistBlobLen(node->zl);
            samples++;
        } while ((node = node->next) && samples < sample_size);
        asize += (double)elesize/samples*ql->len;
    } else if (o->encoding == OBJ_ENCODING_ZIPLIST) {
        asize = sizeof(*o)+ziplistBlobLen(o->ptr);
    } else {
        serverPanic("Unknown list encoding");
    }

1.10.2.2 內存回收

  C 語言並不具備自動的內存回收功能, 所以 redis 在自己的對象系統中構建了一個引用計數(reference counting)技術實現的內存回收機制, 通過這一機制, 程序可以通過跟蹤對象的引用計數信息, 在適當的時候自動釋放對象並進行內存回收。

  • 在創建一個新對象時, 引用計數的值會被初始化爲 1 ;
  • 當對象被一個新程序使用時, 它的引用計數值會被增1;
  • 當對象不再被一個程序使用時, 它的引用計數值會被減1;
  • 當對象的引用計數值變爲 0 時, 對象所佔用的內存會被釋放。

  修改對象引用計數的 API:

函數 作用
incrRefCount 將對象的引用計數值增一。
decrRefCount 將對象的引用計數值減一, 當對象的引用計數值等於 0 時, 釋放對象。
resetRefCount 將對象的引用計數值設置爲 0 , 但並不釋放對象, 這個函數通常在需要重新設置對象的引用計數值時使用。

1.10.2.3 對象共享

  由於引用計數的存在,因此redis中的對象可以被多個數據共享使用,如果真的需要修改的時候再進行深拷貝,即copy-on-write
  當服務器考慮將一個共享對象設置爲鍵的值對象時, 程序需要先檢查給定的共享對象和鍵想創建的目標對象是否完全相同, 只有在共享對象和目標對象完全相同的情況下, 程序纔會將共享對象用作鍵的值對象, 而一個共享對象保存的值越複雜, 驗證共享對象和目標對象是否相同所需的複雜度就會越高, 消耗的 CPU 時間也會越多:

  • 如果共享對象是保存整數值的字符串對象, 那麼驗證操作的複雜度爲 O(1) ;
  • 如果共享對象是保存字符串值的字符串對象, 那麼驗證操作的複雜度爲 O(N) ;
  • 如果共享對象是包含了多個值(或者對象的)對象, 比如列表對象或者哈希對象, 那麼驗證操作的複雜度將會是 O(N2)O(N^2)

  因此, 儘管共享更復雜的對象可以節約更多的內存, 但受到 CPU 時間的限制, redis 只對包含整數值的字符串對象進行共享。

  目前來說, redis 會在初始化服務器時, 創建一萬個字符串對象, 這些對象包含了從 0 到 9999 的所有整數值, 當服務器需要用到值爲 0到 9999 的字符串對象時, 服務器就會使用這些共享對象, 而不是新創建對象。
  創建共享字符串對象的數量可以通過修改 redis.h/OBJ_SHARED_INTEGERS 常量來修改。

1.10.2.4 對象的空轉時長

  除了引用計數,對象中的lru字段也會被用來記性對象的回收。
  如果服務器打開了 maxmemory選項, 並且服務器用於回收內存的算法爲 volatile-lru或者 allkeys-lru, 那麼當服務器佔用的內存數超過了 maxmemory選項所設置的上限值時, 空轉時長較高的那部分鍵會優先被服務器釋放, 從而回收內存。

2 單機數據庫的實現

2.1 數據庫

2.1.1 實現

/* 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 */
    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 */
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
    int id;                     /* Database ID */
    long long avg_ttl;          /* Average TTL, just for stats */
    list *defrag_later;         /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;

  redis是一個鍵值對數據庫服務器,每個數據庫都是由上面的struct redisDb結構表示,其中dict字段是數據庫所有的鍵值對空間。

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

  因此在redis中對數據庫中的數據進行添加,更新,刪除等操作都是對該字典的操作。
  當使用 redis 命令對數據庫進行讀寫時, 服務器不僅會對鍵空間執行指定的讀寫操作, 還會執行一些額外的維護操作, 其中包括:

  • 在讀取一個鍵之後(讀操作和寫操作都要對鍵進行讀取), 服務器會根據鍵是否存在, 以此來更新服務器的鍵空間命中(hit)次數或鍵空間不命中(miss)次數, 這兩個值可以在 INFO stats 命令的 keyspace_hits屬性和 keyspace_misses屬性中查看;
  • 在讀取一個鍵之後, 服務器會更新鍵的 LRU (最後一次使用)時間, 這個值可以用於計算鍵的閒置時間, 使用命令 OBJECT idletime命令可以查看鍵 key 的閒置時間;
  • 如果服務器在讀取一個鍵時, 發現該鍵已經過期, 那麼服務器會先刪除這個過期鍵, 然後才執行餘下的其他操作;
  • 如果有客戶端使用 WATCH 命令監視了某個鍵, 那麼服務器在對被監視的鍵進行修改之後, 會將這個鍵標記爲髒(dirty), 從而讓事務程序注意到這個鍵已經被修改過;
  • 服務器每次修改一個鍵之後, 都會對髒(dirty)鍵計數器的值增一, 這個計數器會觸發服務器的持久化以及複製操作執行;
  • 如果服務器開啓了數據庫通知功能, 那麼在對鍵進行修改之後, 服務器將按配置發送相應的數據庫通知。

2.1.2 數據庫管理


struct redisServer {
    ...
    redisDb *db;
    ...
    int dbnum;                      /* Total number of configured DBs */
    ...
};

  redis中數據庫的管理是通過redisServer中的redisDb數組進行管理,其中存儲着所有的數據庫,dbnum是數據庫的數量,並且每一個數據都有一個自身的id,用戶可以通過該id訪問數據庫。

  • redis 使用惰性刪除和定期刪除兩種策略來刪除過期的鍵: 惰性刪除策略只在碰到過期鍵時才進行刪除操作, 定期刪除策略則每隔一段時間, 主動查找並刪除過期鍵;
  • 執行 SAVE命令或者 BGSAVE命令所產生的新 RDB文件不會包含已經過期的鍵;
  • 執行 BGREWRITEAOF命令所產生的重寫 AOF文件不會包含已經過期的鍵;
  • 當一個過期鍵被刪除之後, 服務器會追加一條 DEL命令到現有AOF文件的末尾, 顯式地刪除過期鍵;
  • 當主服務器刪除一個過期鍵之後, 它會向所有從服務器發送一條 DEL命令, 顯式地刪除過期鍵;
  • 從服務器即使發現過期鍵, 也不會自作主張地刪除它, 而是等待主節點發來 DEL命令, 這種統一、中心化的過期鍵刪除策略可以保證主從服務器數據的一致性;
  • 當redis命令對數據庫進行修改之後, 服務器會根據配置, 向客戶端發送數據庫通知。

2.2 過期鍵刪除策略

2.2.1 刪除策略

  redis對於過期的鍵有三種刪除策略:

  • 定時刪除:在設置鍵的過期時間的同時,創建一個定時器,讓定時器來臨時,立即執行的鍵的刪除;
    • 優點:內存友好能夠及時的刪除不需要的鍵值;
    • 缺點:CPU不友好,創建定時器和大量的集中刪除可能導致服務器的性能下降;
  • 惰性刪除:放任鍵過期不管,每次從鍵空間中獲取鍵值時,都檢查該鍵是否過期,過期則刪除,不過期則返回;
    • 優點:CPU友好,只在讀取時進刪除;
    • 缺點:內存不友好,如果出現不經常訪問的鍵可能永遠也不會刪除;
  • 定期刪除:每隔一段時間就對整個數據庫進行檢查,刪除過期鍵;
    • 優點:可控性強;
    • 缺點:影響服務器的吞吐量和性能。

  redis採用的是惰性刪除和定期刪除配合使用,可以在性能和內存之間權衡。

2.2.2 實現

  惰性刪除的實現如下:

int expireIfNeeded(redisDb *db, robj *key) {
    if (!keyIsExpired(db,key)) return 0;

    /* If we are running in the context of a slave, instead of
     * evicting the expired key from the database, we 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. */
    if (server.masterhost != NULL) return 1;

    /* Delete the 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);
}

  定期刪除實現,該函數每次在服務器執行serverCron時都會執行:

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. */

    int j, iteration = 0;
    int dbs_per_call = CRON_DBS_PER_CALL;
    long long start = ustime(), timelimit, elapsed;

    /* When clients are paused the dataset should be static not just from the
     * POV of clients not being able to write, but also from the POV of
     * expires and evictions of keys not being performed. */
    if (clientsArePaused()) return;

    if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
        /* Don't start a fast cycle if the previous cycle did not exit
         * for time limit. Also don't repeat a fast cycle for the same period
         * as the fast cycle total duration itself. */
        if (!timelimit_exit) return;
        if (start < last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*2) return;
        last_fast_cycle = start;
    }

    /* We usually should test CRON_DBS_PER_CALL per iteration, with
     * two exceptions:
     *
     * 1) Don't test more DBs than we have.
     * 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. */
    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; /* in microseconds. */

    /* Accumulate some global stats as we expire keys, to have some idea
     * about the number of keys that are already logically expired, but still
     * existing inside the database. */
    long total_sampled = 0;
    long total_expired = 0;

    for (j = 0; j < dbs_per_call && timelimit_exit == 0; 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. */
        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;
            iteration++;

            /* If there is nothing to expire try next DB ASAP. */
            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. */
            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_sum = 0;
            ttl_samples = 0;

            if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
                num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;

            while (num--) {
                dictEntry *de;
                long long ttl;

                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++;
            }
            total_expired += expired;

            /* Update the average TTL stats for this database. */
            if (ttl_samples) {
                long long avg_ttl = ttl_sum/ttl_samples;

                /* Do a simple running average with a few samples.
                 * We just use the current estimate with a weight of 2%
                 * and the previous estimate with a weight of 98%. */
                if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
                db->avg_ttl = (db->avg_ttl/50)*49 + (avg_ttl/50);
            }

            /* 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. */
            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;
                }
            }
            /* We don't repeat the cycle if there are less than 25% of keys
             * found expired in the current DB. */
        } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
    }

    elapsed = ustime()-start;
    latencyAddSampleIfNeeded("expire-cycle",elapsed/1000);

    /* Update our estimate of keys existing but yet to be expired.
     * Running average with this sample accounting for 5%. */
    double current_perc;
    if (total_sampled) {
        current_perc = (double)total_expired/total_sampled;
    } else
        current_perc = 0;
    server.stat_expired_stale_perc = (current_perc*0.05)+
                                     (server.stat_expired_stale_perc*0.95);
}

2.3 持久化

2.3.1 RDB持久化

在這裏插入圖片描述

  RDB文件:
在這裏插入圖片描述

  如上圖,redis中的RDB文件的格式包含五個字段,其中兩個爲常量字段(大寫),其他爲變量(小寫):

  • redis:佔5個字節,即redis5個字符;
  • db_version:佔4個字節,是一個用字符串表示的整數,存儲RDB文件的版本號;
  • databases:包含0個或者多個數據庫;
  • EOF:佔1個字節,是個常量,標誌着RDB文件的結束;
  • check_num:佔8個字節,是一個無符號整數,是前4個字段的校驗和。

  databases
在這裏插入圖片描述
  RDB可以包含0個或者多個數據庫,每個數據庫的文件格式如圖中所示包含三個字段:

  • SELECTDB:佔1個字節,用來標識數據庫的開頭;
  • db_number:保存數據庫的號碼,可以是1、2或者5個字節;
  • key_value_pairs:保存了數據庫中所有鍵值對數據。 如果鍵值對帶有過期時間, 那麼過期時間也會和鍵值對保存在一起。 根據鍵值對的數量、類型、內容、以及是否有過期時間等條件的不同, key_value_pairs部分的長度也會有所不同。
#define RDB_OPCODE_SELECTDB   254   /* DB number of the following keys. */
#define RDB_OPCODE_EOF        255   /* End of the RDB file. */

  key_value_pairs
  key_value_pairs部分都保存了一個或以上數量的鍵值對, 如果鍵值對帶有過期時間的話, 那麼鍵值對的過期時間也會被保存在內。

/* Map object types to RDB object types. Macros starting with OBJ_ are for
 * memory storage and may change. Instead RDB types must be fixed because
 * we store them on disk. */
#define RDB_TYPE_STRING 0
#define RDB_TYPE_LIST   1
#define RDB_TYPE_SET    2
#define RDB_TYPE_ZSET   3
#define RDB_TYPE_HASH   4
#define RDB_TYPE_ZSET_2 5 /* ZSET version 2 with doubles stored in binary. */
#define RDB_TYPE_MODULE 6
#define RDB_TYPE_MODULE_2 7 /* Module value with annotations for parsing without

  不帶過期時間的key_value_pairs
在這裏插入圖片描述
  分爲簡單的三個字段:

  • TYPE:佔1個字節,記錄value的類型,是多種編碼類型之一;
  • key:總是一個字符串對象,編碼方式爲RDB_TYPE_STRING
  • value:不同的編碼方式採用不同的數據結構存儲。

  帶過期時間的key_value_pairs

#define RDB_OPCODE_EXPIRETIME_MS 252    /* Expire time in milliseconds. */
#define RDB_OPCODE_EXPIRETIME 253       /* Old expire time in seconds. */

在這裏插入圖片描述

  • EXPIRETIME:佔1個字節,可選爲RDB_OPCODE_EXPIRETIME_MSRDB_OPCODE_EXPIRETIME,表示延時的計時單位;
  • ms:佔8字節,是一個帶符號的整數,記錄着一個以毫秒爲單位的 UNIX 時間戳, 這個時間戳就是鍵值對的過期時間;
  • type:同上;
  • key:同上;
  • value:同上。

  字符串:
  需要注意的字符串編碼相對來說不太一樣。字符串編碼分爲int,raw,embstr但是這裏分爲int,raw,lzf三種:

  • int:當字符串對象保存的是長度不超過32位的整數時採用,存儲結構如下圖;
    在這裏插入圖片描述
  • raw
    • 如果字符串的長度小於等於 20 字節, 那麼這個字符串會直接被原樣保存;
    • 如果字符串的長度大於20字節,並且服務器開啓了rdbcompression壓縮選項,則使用壓縮格式存儲;

在這裏插入圖片描述

  • lzf:如果字符串的長度大於20字節,並且服務器開啓了rdbcompression壓縮選項,則使用壓縮格式存儲。
    在這裏插入圖片描述

  上面幾個圖的各個字段的含義正如其中的文本所描述。

/* When a length of a string object stored on disk has the first two bits
 * set, the remaining six bits specify a special encoding for the object
 * accordingly to the following defines: */
#define RDB_ENC_INT8 0        /* 8 bit signed integer */
#define RDB_ENC_INT16 1       /* 16 bit signed integer */
#define RDB_ENC_INT32 2       /* 32 bit signed integer */
#define RDB_ENC_LZF 3         /* string compressed with FASTLZ */

  總結

  • RDB文件用於保存和還原 redis 服務器所有數據庫中的所有鍵值對數據;
  • SAVE命令由服務器進程直接執行保存操作,所以該命令會阻塞服務器;
  • BGSAVE命令由子進程執行保存操作,所以該命令不會阻塞服務器;
  • 服務器狀態中會保存所有用 save 選項設置的保存條件,當任意一個保存條件被滿足時,服務器會自動執行 BGSAVE 命令;
  • RDB文件是一個經過壓縮的二進制文件,由多個部分組成;
  • 對於不同類型的鍵值對, RDB 文件會使用不同的方式來保存它們。

2.3.2 AOF持久化

  AOF :redis 默認不開啓。它的出現是爲了彌補RDB的不足(數據的不一致性),所以它採用日誌的形式來記錄每個寫操作,並追加到文件中。redis 重啓的會根據日誌文件的內容將寫指令從前到後執行一次以完成數據的恢復工作。

  AOF持久化功能的實現可以分爲命令追加(append)、文件寫入、文件同步(sync)三個步驟。
  命令追加

struct redisServer {
    ...
    sds aof_buf;      /* AOF buffer, written before entering the event loop */
    ...
};

  當 AOF持久化功能處於打開狀態時, 服務器在執行完一個寫命令之後, 會以協議格式將被執行的寫命令追加到服務器狀態的aof_buf緩衝區的末尾。

  文件寫入與同步:
  redis 的服務器進程就是一個事件循環(loop), 這個循環中的文件事件負責接收客戶端的命令請求, 以及向客戶端發送命令回覆, 而時間事件則負責執行像 serverCron函數這樣需要定時運行的函數。

因爲服務器在處理文件事件時可能會執行寫命令, 使得一些內容被追加到 aof_buf緩衝區裏面, 所以在服務器每次結束一個事件循環之前, 它都會調用flushAppendOnlyFile函數, 考慮是否需要將 aof_buf緩衝區中的內容寫入和保存到 AOF 文件裏面。

//僞代碼
def eventLoop():
    while True:
        # 處理文件事件,接收命令請求以及發送命令回覆
        # 處理命令請求時可能會有新內容被追加到 aof_buf 緩衝區中
        processFileEvents()
        # 處理時間事件
        processTimeEvents()
        # 考慮是否要將 aof_buf 中的內容寫入和保存到 AOF 文件裏面
        flushAppendOnlyFile()

  flushAppendOnlyFile函數的行爲由服務器配置的 appendfsync選項的值來決定, 各個不同值產生的行爲如下表所示:

appendfsync 選項的值 flushAppendOnlyFile 函數的行爲
always aof_buf緩衝區中的所有內容寫入並同步到 AOF 文件。
everysec 將 aof_buf 緩衝區中的所有內容寫入到 AOF文件, 如果上次同步 AOF文件的時間距離現在超過一秒鐘, 那麼再次對 AOF文件進行同步, 並且這個同步操作是由一個線程專門負責執行的。
no aof_buf緩衝區中的所有內容寫入到 AOF文件, 但並不對 AOF文件進行同步, 何時同步由操作系統來決定。

  如果用戶沒有主動爲appendfsync選項設置值, 那麼appendfsync選項的默認值爲 everysec, 關於 appendfsync選項的更多信息, 請參考 redis 項目附帶的示例配置文件 redis.conf
  爲了提高文件的寫入效率, 在現代操作系統中, 當用戶調用 write函數, 將一些數據寫入到文件的時候, 操作系統通常會將寫入數據暫時保存在一個內存緩衝區裏面, 等到緩衝區的空間被填滿、或者超過了指定的時限之後, 才真正地將緩衝區中的數據寫入到磁盤裏面。
  這種做法雖然提高了效率, 但也爲寫入數據帶來了安全問題, 因爲如果計算機發生停機, 那麼保存在內存緩衝區裏面的寫入數據將會丟失。
  爲此, 系統提供了 fsyncfdatasync兩個同步函數, 它們可以強制讓操作系統立即將緩衝區中的數據寫入到硬盤裏面, 從而確保寫入數據的安全性。

  服務器配置 appendfsync 選項的值直接決定 AOF 持久化功能的效率和安全性。

  • appendfsync == always時, 服務器在每個事件循環都要將 aof_buf緩衝區中的所有內容寫入到 AOF文件, 並且同步 AOF文件, 所以always的效率是 appendfsync選項三個值當中最慢的一個, 但從安全性來說, always也是最安全的, 因爲即使出現故障停機, AOF持久化也只會丟失一個事件循環中所產生的命令數據。
  • appendfsync == everysec時, 服務器在每個事件循環都要將 aof_buf緩衝區中的所有內容寫入到AOF文件, 並且每隔超過一秒就要在子線程中對 AOF文件進行一次同步: 從效率上來講, everysec模式足夠快, 並且就算出現故障停機, 數據庫也只丟失一秒鐘的命令數據。
  • appendfsync == no時, 服務器在每個事件循環都要將 aof_buf緩衝區中的所有內容寫入到 AOF文件, 至於何時對 AOF文件進行同步, 則由操作系統控制。因爲處於 no模式下的 flushAppendOnlyFile調用無須執行同步操作, 所以該模式下的 AOF文件寫入速度總是最快的, 不過因爲這種模式會在系統緩存中積累一段時間的寫入數據, 所以該模式的單次同步時長通常是三種模式中時間最長的: 從平攤操作的角度來看,no模式和 everysec模式的效率類似, 當出現故障停機時, 使用 no模式的服務器將丟失上次同步 AOF文件之後的所有寫命令數據。

  在執行 BGREWRITEAOF 命令時, redis 服務器會維護一個 AOF 重寫緩衝區, 該緩衝區會在子進程創建新 AOF 文件的期間, 記錄服務器執行的所有寫命令。 當子進程完成創建新 AOF 文件的工作之後, 服務器會將重寫緩衝區中的所有內容追加到新 AOF 文件的末尾, 使得新舊兩個 AOF 文件所保存的數據庫狀態一致。 最後, 服務器用新的 AOF 文件替換舊的 AOF 文件, 以此來完成 AOF 文件重寫操作。

2.4 事件

2.4.1 簡介

  Reactor 的一般工作過程是首先在 Reactor 中註冊(Reactor)感興趣事件,並在註冊時候指定某個已定義的回調函數(callback);當客戶端發送請求時,在 Reactor 中會觸發剛纔註冊的事件,並調用對應的處理函數。在這一個處理回調函數中,一般會有數據接收、處理、回覆請求等操作。

  redis 基於Reactor模式開發了自己的網絡事件處理器: 這個處理器被稱爲文件事件處理器(file event handler):

  • 文件事件處理器使用 I/O 多路複用(multiplexing)程序來同時監聽多個套接字, 並根據套接字目前執行的任務來爲套接字關聯不同的事件處理器;
  • 當被監聽的套接字準備好執行連接應答(accept)、讀取(read)、寫入(write)、關閉(close)等操作時, 與操作相對應的文件事件就會產生, 這時文件事件處理器就會調用套接字之前關聯好的事件處理器來處理這些事件。

  雖然文件事件處理器以單線程方式運行, 但通過使用I/O多路複用程序來監聽多個套接字, 文件事件處理器既實現了高性能的網絡通信模型, 又可以很好地與 redis 服務器中其他同樣以單線程方式運行的模塊進行對接, 這保持了 redis 內部單線程設計的簡單性。

2.4.2 文件事件的構成

  文件事件處理器的四個組成部分:套接字、 I/O 多路複用程序、 文件事件分派器(dispatcher)、 以及事件處理器。
在這裏插入圖片描述

  文件事件是對套接字操作的抽象, 每當一個套接字準備好執行連接應答(accept)、寫入、讀取、關閉等操作時, 就會產生一個文件事件。 因爲一個服務器通常會連接多個套接字, 所以多個文件事件有可能會併發地出現。
  I/O 多路複用程序負責監聽多個套接字, 並向文件事件分派器傳送那些產生了事件的套接字。
  儘管多個文件事件可能會併發地出現, 但 I/O 多路複用程序總是會將所有產生事件的套接字都入隊到一個隊列裏面, 然後通過這個隊列, 以有序(sequentially)、同步(synchronously)、每次一個套接字的方式向文件事件分派器傳送套接字: 當上一個套接字產生的事件被處理完畢之後(該套接字爲事件所關聯的事件處理器執行完畢), I/O 多路複用程序纔會繼續向文件事件分派器傳送下一個套接字。
  文件事件分派器接收 I/O 多路複用程序傳來的套接字, 並根據套接字產生的事件的類型, 調用相應的事件處理器。
  服務器會爲執行不同任務的套接字關聯不同的事件處理器, 這些處理器是一個個函數, 它們定義了某個事件發生時, 服務器應該執行的動作。

2.4.3 IO多路複用的實現

  redis 的 I/O 多路複用程序的所有功能都是通過包裝常見的 selectepollevportkqueue這些 I/O多路複用函數庫來實現的, 每個 I/O 多路複用函數庫在 redis 源碼中都對應一個單獨的文件, 比如ae_select.cae_epoll.cae_kqueue.c, 諸如此類。
  因爲 redis 爲每個 I/O 多路複用函數庫都實現了相同的 API , 所以 I/O 多路複用程序的底層實現是可以互換的。

#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
    #ifdef HAVE_EPOLL
    #include "ae_epoll.c"
    #else
        #ifdef HAVE_KQUEUE
        #include "ae_kqueue.c"
        #else
        #include "ae_select.c"
        #endif
    #endif
#endif

2.4.4 事件類型及其API

  事件類型
  I/O 多路複用程序可以監聽多個套接字的 ae.h/AE_READABLE事件和ae.h/AE_WRITABLE事件, 這兩類事件和套接字操作之間的對應關係:

  • 當套接字變得可讀時(客戶端對套接字執行write操作,或者執行close操作), 或者有新的可應答(acceptable)套接字出現時(客戶端對服務器的監聽套接字執行 connect操作), 套接字產生 AE_READABLE事件;
  • 當套接字變得可寫時(客戶端對套接字執行 read操作), 套接字產生 AE_WRITABLE事件。

  I/O 多路複用程序允許服務器同時監聽套接字的 AE_READABLE事件和 AE_WRITABLE事件, 如果一個套接字同時產生了這兩種事件, 那麼文件事件分派器會優先處理 AE_READABLE事件, 等到 AE_READABLE事件處理完之後, 才處理 AE_WRITABLE事件。

  API

  • ae.c/aeCreateFileEvent函數接受一個套接字描述符、 一個事件類型、 以及一個事件處理器作爲參數, 將給定套接字的給定事件加入到 I/O 多路複用程序的監聽範圍之內, 並對事件和事件處理器進行關聯;
  • ae.c/aeDeleteFileEvent函數接受一個套接字描述符和一個監聽事件類型作爲參數, 讓 I/O 多路複用程序取消對給定套接字的給定事件的監聽, 並取消事件和事件處理器之間的關聯;
  • ae.c/aeGetFileEvents函數接受一個套接字描述符, 返回該套接字正在被監聽的事件類型:
    • 如果套接字沒有任何事件被監聽, 那麼函數返回 AE_NONE
    • 如果套接字的讀事件正在被監聽, 那麼函數返回 AE_READABLE
    • 如果套接字的寫事件正在被監聽, 那麼函數返回 AE_WRITABLE
    • 如果套接字的讀事件和寫事件正在被監聽, 那麼函數返回AE_READABLE | AE_WRITABLE
  • ae.c/aeWait函數接受一個套接字描述符、一個事件類型和一個毫秒數爲參數, 在給定的時間內阻塞並等待套接字的給定類型事件產生, 當事件成功產生, 或者等待超時之後, 函數返回;
  • ae.c/aeApiPoll函數接受一個 sys/time.h/struct timeval結構爲參數, 並在指定的時間內, 阻塞並等待所有被 aeCreateFileEvent函數設置爲監聽狀態的套接字產生文件事件, 當有至少一個事件產生, 或者等待超時後, 函數返回;
  • ae.c/aeProcessEvents函數是文件事件分派器, 它先調用 aeApiPoll函數來等待事件產生, 然後遍歷所有已產生的事件, 並調用相應的事件處理器來處理這些事件;
  • ae.c/aeGetApiName函數返回 I/O 多路複用程序底層所使用的 I/O 多路複用函數庫的名稱: 返回 “epoll” 表示底層爲 epoll函數庫, 返回"select" 表示底層爲 select函數庫, 諸如此類。

2.4.5 文件事件處理器

  redis 爲文件事件編寫了多個處理器, 這些事件處理器分別用於實現不同的網絡通訊需求:

  • 爲了對連接服務器的各個客戶端進行應答, 服務器要爲監聽套接字關聯連接應答處理器;
  • 爲了接收客戶端傳來的命令請求, 服務器要爲客戶端套接字關聯命令請求處理器;
  • 爲了向客戶端返回命令的執行結果, 服務器要爲客戶端套接字關聯命令回覆處理器;
  • 當主服務器和從服務器進行復制操作時, 主從服務器都需要關聯特別爲複製功能編寫的複製處理器;
  • 等等。

2.4.5.1 連接應答處理器

  networking.c/acceptTcpHandler函數是 redis 的連接應答處理器, 這個處理器用於對連接服務器監聽套接字的客戶端進行應答, 具體實現爲sys/socket.h/accept函數的包裝。

  當 redis 服務器進行初始化的時候, 程序會將這個連接應答處理器和服務器監聽套接字的 AE_READABLE事件關聯起來, 當有客戶端用sys/socket.h/connect函數連接服務器監聽套接字的時候, 套接字就會產生 AE_READABLE事件, 引發連接應答處理器執行, 並執行相應的套接字應答操作。

void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
    int cport, cfd, max = MAX_ACCEPTS_PER_CALL;
    char cip[NET_IP_STR_LEN];
    UNUSED(el);
    UNUSED(mask);
    UNUSED(privdata);

    while(max--) {
        cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
        if (cfd == ANET_ERR) {
            if (errno != EWOULDBLOCK)
                serverLog(LL_WARNING,
                    "Accepting client connection: %s", server.neterr);
            return;
        }
        serverLog(LL_VERBOSE,"Accepted %s:%d", cip, cport);
        acceptCommonHandler(cfd,0,cip);
    }
}

2.4.5.2 命令請求處理器

  networking.c/readQueryFromClient函數是 Redis 的命令請求處理器, 這個處理器負責從套接字中讀入客戶端發送的命令請求內容, 具體實現爲 unistd.h/read函數的包裝。
  當一個客戶端通過連接應答處理器成功連接到服務器之後, 服務器會將客戶端套接字的 AE_READABLE事件和命令請求處理器關聯起來, 當客戶端向服務器發送命令請求的時候, 套接字就會產生 AE_READABLE事件, 引發命令請求處理器執行, 並執行相應的套接字讀入操作。
  在客戶端連接服務器的整個過程中, 服務器都會一直爲客戶端套接字的 AE_READABLE事件關聯命令請求處理器。

void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
    client *c = (client*) privdata;
    int nread, readlen;
    size_t qblen;
    UNUSED(el);
    UNUSED(mask);

    readlen = PROTO_IOBUF_LEN;
    /* If this is a multi bulk request, and we are processing a bulk reply
     * that is large enough, try to maximize the probability that the query
     * buffer contains exactly the SDS string representing the object, even
     * at the risk of requiring more read(2) calls. This way the function
     * processMultiBulkBuffer() can avoid copying buffers to create the
     * Redis Object representing the argument. */
    if (c->reqtype == PROTO_REQ_MULTIBULK && c->multibulklen && c->bulklen != -1
        && c->bulklen >= PROTO_MBULK_BIG_ARG)
    {
        ssize_t remaining = (size_t)(c->bulklen+2)-sdslen(c->querybuf);

        /* Note that the 'remaining' variable may be zero in some edge case,
         * for example once we resume a blocked client after CLIENT PAUSE. */
        if (remaining > 0 && remaining < readlen) readlen = remaining;
    }

    qblen = sdslen(c->querybuf);
    if (c->querybuf_peak < qblen) c->querybuf_peak = qblen;
    c->querybuf = sdsMakeRoomFor(c->querybuf, readlen);
    nread = read(fd, c->querybuf+qblen, readlen);
    if (nread == -1) {
        if (errno == EAGAIN) {
            return;
        } else {
            serverLog(LL_VERBOSE, "Reading from client: %s",strerror(errno));
            freeClient(c);
            return;
        }
    } else if (nread == 0) {
        serverLog(LL_VERBOSE, "Client closed connection");
        freeClient(c);
        return;
    } else if (c->flags & CLIENT_MASTER) {
        /* Append the query buffer to the pending (not applied) buffer
         * of the master. We'll use this buffer later in order to have a
         * copy of the string applied by the last command executed. */
        c->pending_querybuf = sdscatlen(c->pending_querybuf,
                                        c->querybuf+qblen,nread);
    }

    sdsIncrLen(c->querybuf,nread);
    c->lastinteraction = server.unixtime;
    if (c->flags & CLIENT_MASTER) c->read_reploff += nread;
    server.stat_net_input_bytes += nread;
    if (sdslen(c->querybuf) > server.client_max_querybuf_len) {
        sds ci = catClientInfoString(sdsempty(),c), bytes = sdsempty();

        bytes = sdscatrepr(bytes,c->querybuf,64);
        serverLog(LL_WARNING,"Closing client that reached max query buffer length: %s (qbuf initial bytes: %s)", ci, bytes);
        sdsfree(ci);
        sdsfree(bytes);
        freeClient(c);
        return;
    }

    /* Time to process the buffer. If the client is a master we need to
     * compute the difference between the applied offset before and after
     * processing the buffer, to understand how much of the replication stream
     * was actually applied to the master state: this quantity, and its
     * corresponding part of the replication stream, will be propagated to
     * the sub-slaves and to the replication backlog. */
    processInputBufferAndReplicate(c);
}

2.4.5.3 命令回覆處理器

  networking.c/sendReplyToClient函數是 Redis 的命令回覆處理器, 這個處理器負責將服務器執行命令後得到的命令回覆通過套接字返回給客戶端, 具體實現爲 unistd.h/write函數的包裝。
  當服務器有命令回覆需要傳送給客戶端的時候, 服務器會將客戶端套接字的 AE_WRITABLE事件和命令回覆處理器關聯起來, 當客戶端準備好接收服務器傳回的命令回覆時, 就會產生 AE_WRITABLE事件, 引發命令回覆處理器執行, 並執行相應的套接字寫入操作。
  當命令回覆發送完畢之後, 服務器就會解除命令回覆處理器與客戶端套接字的 AE_WRITABLE事件之間的關聯。

/* Write event handler. Just send data to the client. */
void sendReplyToClient(aeEventLoop *el, int fd, void *privdata, int mask) {
    UNUSED(el);
    UNUSED(mask);
    writeToClient(fd,privdata,1);
}

2.4.5.4 總結

  • Redis 服務器是一個事件驅動程序, 服務器處理的事件分爲時間事件和文件事件兩類;
  • 文件事件處理器是基於 Reactor模式實現的網絡通訊程序;
  • 文件事件是對套接字操作的抽象: 每次套接字變得可應答(acceptable)、可寫(writable)或者可讀(readable)時, 相應的文件事件就會產生;
  • 文件事件分爲 AE_READABLE事件(讀事件)和 AE_WRITABLE事件(寫事件)兩類;
  • 時間事件分爲定時事件和週期性事件: 定時事件只在指定的時間達到一次, 而週期性事件則每隔一段時間到達一次;
  • 服務器在一般情況下只執行 serverCron函數一個時間事件, 並且這個事件是週期性事件;
  • 文件事件和時間事件之間是合作關係, 服務器會輪流處理這兩種事件, 並且處理事件的過程中也不會進行搶佔;
  • 時間事件的實際處理時間通常會比設定的到達時間晚一些。

2.4.6 時間事件

2.4.6.1 組成和實現

  redis時間事件分爲兩類:

  • 定時時間:一段時間後執行一次;
  • 週期性事件:每隔一段時間執行一次。

  時間事件組成:

/* Time event structure */
typedef struct aeTimeEvent {
    long long id; /* time event identifier. */
    long when_sec; /* seconds */
    long when_ms; /* milliseconds */
    aeTimeProc *timeProc;
    aeEventFinalizerProc *finalizerProc;
    void *clientData;
    struct aeTimeEvent *prev;
    struct aeTimeEvent *next;
} aeTimeEvent;

  可以看到事件就是一個雙向鏈表,新的時間事件總是插入到表頭。每當時間事件執行器運行時就遍歷整個鏈表,查找已經達到時間的事件,並調用響應的事件處理器。

typedef void aeEventFinalizerProc(struct aeEventLoop *eventLoop, void *clientData);
typedef void aeBeforeSleepProc(struct aeEventLoop *eventLoop);

2.4.6.2 serverCron

  持續運行的redis服務器需要定期對自身的資源和狀態進行檢查和調整,從而確保服務可以長期穩定的運行,這些定期操作由serverCron負責:

  • 更新服務器的各類統計信息,如時間,內存佔用,數據庫佔用等;
  • 清理數據庫中的過期鍵值對;
  • 關閉和清理鏈接失效的客戶端;
  • 嘗試進行AOF,RDB持久化;
  • 如果是主服務器則對從服務器進行定期同步;
  • 如果處於集羣模式,對集羣進行定期同步和鏈接測試。

2.4.7 事件的調度

  redis調度的僞代碼如下:

def aeProcessEvents():
    //尋找離當前時間最近的時間事件
    time_eve = aeSearchNearestTimer()
    //計算距離
    remaid_tim = tim_eve - unix_ts_now()
    if remaid_tim < 0:
        remaid_tim = 0

    time_val = create_timeval_ms(remaid_tim)
    //阻塞並等待文件事件產生
    aeApiPoll(time_val)
    //處理已產生的文件事件
    processFileEvents()
    //處理所有已經到達的時間事件
    processTimeEvents()

  這樣做的一個好處:

  • 避免輪詢造成的忙等待,阻塞時間由最接近的時間事件決定,而且服務器本身維護serverCron事件,一定會在較短的時間內完成事件處理;

2.5 客戶端屬性

  客戶端屬性分爲兩類:特定屬性和通用屬性(顧名思義)。

/* With multiplexing we need to take per-client state.
 * Clients are taken in a linked list. */
typedef struct client {
    uint64_t id;            /* Client incremental unique ID. */
    int fd;                 /* Client socket. */
    redisDb *db;            /* Pointer to currently SELECTed DB. */
    robj *name;             /* As set by CLIENT SETNAME. */
    sds querybuf;           /* Buffer we use to accumulate client queries. */
    size_t qb_pos;          /* The position we have read in querybuf. */
    sds pending_querybuf;   /* If this client is flagged as master, this buffer
                               represents the yet not applied portion of the
                               replication stream that we are receiving from
                               the master. */
    size_t querybuf_peak;   /* Recent (100ms or more) peak of querybuf size. */
    int argc;               /* Num of arguments of current command. */
    robj **argv;            /* Arguments of current command. */
    struct redisCommand *cmd, *lastcmd;  /* Last command executed. */
    int reqtype;            /* Request protocol type: PROTO_REQ_* */
    int multibulklen;       /* Number of multi bulk arguments left to read. */
    long bulklen;           /* Length of bulk argument in multi bulk request. */
    list *reply;            /* List of reply objects to send to the client. */
    unsigned long long reply_bytes; /* Tot bytes of objects in reply list. */
    size_t sentlen;         /* Amount of bytes already sent in the current
                               buffer or object being sent. */
    time_t ctime;           /* Client creation time. */
    time_t lastinteraction; /* Time of the last interaction, used for timeout */
    time_t obuf_soft_limit_reached_time;
    int flags;              /* Client flags: CLIENT_* macros. */
    int authenticated;      /* When requirepass is non-NULL. */
    int replstate;          /* Replication state if this is a slave. */
    int repl_put_online_on_ack; /* Install slave write handler on ACK. */
    int repldbfd;           /* Replication DB file descriptor. */
    off_t repldboff;        /* Replication DB file offset. */
    off_t repldbsize;       /* Replication DB file size. */
    sds replpreamble;       /* Replication DB preamble. */
    long long read_reploff; /* Read replication offset if this is a master. */
    long long reploff;      /* Applied replication offset if this is a master. */
    long long repl_ack_off; /* Replication ack offset, if this is a slave. */
    long long repl_ack_time;/* Replication ack time, if this is a slave. */
    long long psync_initial_offset; /* FULLRESYNC reply offset other slaves
                                       copying this slave output buffer
                                       should use. */
    char replid[CONFIG_RUN_ID_SIZE+1]; /* Master replication ID (if master). */
    int slave_listening_port; /* As configured with: SLAVECONF listening-port */
    char slave_ip[NET_IP_STR_LEN]; /* Optionally given by REPLCONF ip-address */
    int slave_capa;         /* Slave capabilities: SLAVE_CAPA_* bitwise OR. */
    multiState mstate;      /* MULTI/EXEC state */
    int btype;              /* Type of blocking op if CLIENT_BLOCKED. */
    blockingState bpop;     /* blocking state */
    long long woff;         /* Last write global replication offset. */
    list *watched_keys;     /* Keys WATCHED for MULTI/EXEC CAS */
    dict *pubsub_channels;  /* channels a client is interested in (SUBSCRIBE) */
    list *pubsub_patterns;  /* patterns a client is interested in (SUBSCRIBE) */
    sds peerid;             /* Cached peer ID. */
    listNode *client_list_node; /* list node in client list */

    /* Response buffer */
    int bufpos;
    char buf[PROTO_REPLY_CHUNK_BYTES];
} client;

2.5.1 socket描述符

  客戶端狀態的 fd屬性記錄了客戶端正在使用的套接字描述符。根據客戶端類型的不同, fd屬性的值可以是 -1或者是大於 -1的整數:

  • 僞客戶端(fake client)的 fd屬性的值爲 -1: 僞客戶端處理的命令請求來源於 AOF文件或者 Lua腳本, 而不是網絡, 所以這種客戶端不需要套接字連接, 自然也不需要記錄套接字描述符。 目前 Redis 服務器會在兩個地方用到僞客戶端, 一個用於載入 AOF 文件並還原數據庫狀態, 而另一個則用於執行 Lua 腳本中包含的 Redis 命令;
  • 普通客戶端的 fd屬性的值爲大於 -1的整數: 普通客戶端使用套接字來與服務器進行通訊, 所以服務器會用 fd屬性來記錄客戶端套接字的描述符。 因爲合法的套接字描述符不能是 -1 , 所以普通客戶端的套接字描述符的值必然是大於 -1的整數。

2.5.2 名字

  名字可以用來標示客戶端,方便操作。在默認情況下, 一個連接到服務器的客戶端是沒有名字的。

2.5.3 標誌

/* Client flags */
#define CLIENT_SLAVE (1<<0)   /* This client is a slave server */
#define CLIENT_MASTER (1<<1)  /* This client is a master server */
#define CLIENT_MONITOR (1<<2) /* This client is a slave monitor, see MONITOR */
#define CLIENT_MULTI (1<<3)   /* This client is in a MULTI context */
#define CLIENT_BLOCKED (1<<4) /* The client is waiting in a blocking operation */
#define CLIENT_DIRTY_CAS (1<<5) /* Watched keys modified. EXEC will fail. */
#define CLIENT_CLOSE_AFTER_REPLY (1<<6) /* Close after writing entire reply. */
#define CLIENT_UNBLOCKED (1<<7) /* This client was unblocked and is stored in
                                  server.unblocked_clients */
#define CLIENT_LUA (1<<8) /* This is a non connected client used by Lua */
#define CLIENT_ASKING (1<<9)     /* Client issued the ASKING command */
#define CLIENT_CLOSE_ASAP (1<<10)/* Close this client ASAP */
#define CLIENT_UNIX_SOCKET (1<<11) /* Client connected via Unix domain socket */
#define CLIENT_DIRTY_EXEC (1<<12)  /* EXEC will fail for errors while queueing */
#define CLIENT_MASTER_FORCE_REPLY (1<<13)  /* Queue replies even if is master */
#define CLIENT_FORCE_AOF (1<<14)   /* Force AOF propagation of current cmd. */
#define CLIENT_FORCE_REPL (1<<15)  /* Force replication of current cmd. */
#define CLIENT_PRE_PSYNC (1<<16)   /* Instance don't understand PSYNC. */
#define CLIENT_READONLY (1<<17)    /* Cluster client is in read-only state. */
#define CLIENT_PUBSUB (1<<18)      /* Client is in Pub/Sub mode. */
#define CLIENT_PREVENT_AOF_PROP (1<<19)  /* Don't propagate to AOF. */
#define CLIENT_PREVENT_REPL_PROP (1<<20)  /* Don't propagate to slaves. */
#define CLIENT_PREVENT_PROP (CLIENT_PREVENT_AOF_PROP|CLIENT_PREVENT_REPL_PROP)
#define CLIENT_PENDING_WRITE (1<<21) /* Client has output to send but a write
                                        handler is yet not installed. */
#define CLIENT_REPLY_OFF (1<<22)   /* Don't send replies to client. */
#define CLIENT_REPLY_SKIP_NEXT (1<<23)  /* Set CLIENT_REPLY_SKIP for next cmd */
#define CLIENT_REPLY_SKIP (1<<24)  /* Don't send just this reply. */
#define CLIENT_LUA_DEBUG (1<<25)  /* Run EVAL in debug mode. */
#define CLIENT_LUA_DEBUG_SYNC (1<<26)  /* EVAL debugging without fork() */
#define CLIENT_MODULE (1<<27) /* Non connected client used by some module. */
#define CLIENT_PROTECTED (1<<28) /* Client should not be freed for now. */

  客戶端的標誌屬性 flags記錄了客戶端的角色(role), 以及客戶端目前所處的狀態。
  每個標誌使用一個常量表示, 一部分標誌記錄了客戶端的角色:

  • 在主從服務器進行復制操作時, 主服務器會成爲從服務器的客戶端, 而從服務器也會成爲主服務器的客戶端。 CLIENT_MASTER標誌表示客戶端代表的是一個主服務器, CLIENT_SLAVE標誌表示客戶端代表的是一個從服務器;
  • CLIENT_PRE_PSYNC標誌表示客戶端代表的是一個版本低於 Redis 2.8 的從服務器, 主服務器不能使用 PSYNC命令與這個從服務器進行同步。 這個標誌只能在 CLIENT_SLAVE標誌處於打開狀態時使用;
  • CLIENT_LUA_CLIENT標識表示客戶端是專門用於處理 Lua 腳本里麪包含的 Redis 命令的僞客戶端;

  而另外一部分標誌則記錄了客戶端目前所處的狀態:

  • CLIENT_MONITOR標誌表示客戶端正在執行MONITOR命令;
  • CLIENT_UNIX_SOCKET標誌表示服務器使用 UNIX套接字來連接客戶端;
  • CLIENT_BLOCKED標誌表示客戶端正在被 BRPOPBLPOP等命令阻塞;
  • CLIENT_UNBLOCKED標誌表示客戶端已經從 CLIENT_BLOCKED標誌所表示的阻塞狀態中脫離出來, 不再阻塞。 CLIENT_UNBLOCKED標誌只能在CLIENT_BLOCKED標誌已經打開的情況下使用;
  • CLIENT_MULTI標誌表示客戶端正在執行事務;
  • CLIENT_DIRTY_CAS標誌表示事務使用 WATCH命令監視的數據庫鍵已經被修改, CLIENT_DIRTY_EXEC標誌表示事務在命令入隊時出現了錯誤, 以上兩個標誌都表示事務的安全性已經被破壞, 只要這兩個標記中的任意一個被打開, EXEC命令必然會執行失敗。 這兩個標誌只能在客戶端打開了 CLIENT_MULTI標誌的情況下使用;
  • CLIENT_CLOSE_ASAP標誌表示客戶端的輸出緩衝區大小超出了服務器允許的範圍, 服務器會在下一次執行 serverCron函數時關閉這個客戶端, 以免服務器的穩定性受到這個客戶端影響。 積存在輸出緩衝區中的所有內容會直接被釋放, 不會返回給客戶端;
  • CLIENT_CLOSE_AFTER_REPLY標誌表示有用戶對這個客戶端執行了CLIENT_KILL命令, 或者客戶端發送給服務器的命令請求中包含了錯誤的協議內容。 服務器會將客戶端積存在輸出緩衝區中的所有內容發送給客戶端, 然後關閉客戶端;
  • CLIENT_ASKING標誌表示客戶端向集羣節點(運行在集羣模式下的服務器)發送了 ASKING 命令;
  • CLIENT_FORCE_AOF標誌強制服務器將當前執行的命令寫入到 AOF文件裏面, CLIENT_FORCE_REPL標誌強制主服務器將當前執行的命令複製給所有從服務器。 執行 PUBSUB命令會使客戶端打開 CLIENT_FORCE_AOF標誌, 執行 SCRIPT_LOAD命令會使客戶端打開 CLIENT_FORCE_AOF標誌和 CLIENT_FORCE_REPL標誌;
  • 在主從服務器進行命令傳播期間, 從服務器需要向主服務器發送 REPLICATION ACK命令, 在發送這個命令之前, 從服務器必須打開主服務器對應的客戶端的 CLIENT_MASTER_FORCE_REPLY標誌, 否則發送操作會被拒絕執行。

  PUBSUB命令和 SCRIPT LOAD命令的特殊性:
  通常情況下, Redis 只會將那些對數據庫進行了修改的命令寫入到 AOF 文件, 並複製到各個從服務器: 如果一個命令沒有對數據庫進行任何修改, 那麼它就會被認爲是隻讀命令, 這個命令不會被寫入到 AOF文件, 也不會被複制到從服務器。
  以上規則適用於絕大部分 Redis 命令, 但 PUBSUB命令和 SCRIPT_LOAD命令是其中的例外。
  PUBSUB命令雖然沒有修改數據庫, 但 PUBSUB命令向頻道的所有訂閱者發送消息這一行爲帶有副作用, 接收到消息的所有客戶端的狀態都會因爲這個命令而改變。 因此, 服務器需要使用 REDIS_FORCE_AOF標誌, 強制將這個命令寫入 AOF文件, 這樣在將來載入 AOF文件時, 服務器就可以再次執行相同的 PUBSUB命令, 併產生相同的副作用。
  SCRIPT_LOAD命令的情況與 PUBSUB命令類似: 雖然 SCRIPT_LOAD命令沒有修改數據庫, 但它修改了服務器狀態, 所以它是一個帶有副作用的命令, 服務器需要使用 REDIS_FORCE_AOF標誌, 強制將這個命令寫入 AOF文件, 使得將來在載入 AOF文件時, 服務器可以產生相同的副作用。
  另外, 爲了讓主服務器和從服務器都可以正確地載入 SCRIPT_LOAD命令指定的腳本, 服務器需要使用 REDIS_FORCE_REPL標誌, 強制將SCRIPT_LOAD命令複製給所有從服務器。

2.5.4 輸入緩衝區

  客戶端狀態的輸入緩衝區用於保存客戶端發送的命令請求(querybuf),輸入緩衝區的大小會根據輸入內容動態地縮小或者擴大, 但它的最大大小不能超過 1 GB , 否則服務器將關閉這個客戶端。

2.5.5 命令與命令參數

  在服務器將客戶端發送的命令請求保存到客戶端狀態的 querybuf屬性之後, 服務器將對命令請求的內容進行分析, 並將得出的命令參數以及命令參數的個數分別保存到客戶端狀態的 argv屬性和 argc屬性:
  argv屬性是一個數組, 數組中的每個項都是一個字符串對象: 其中 argv[0]是要執行的命令, 而之後的其他項則是傳給命令的參數。argc屬性則負責記錄 argv數組的長度。

2.5.6 命令實現函數

  當服務器從協議內容中分析並得出 argv屬性和 argc屬性的值之後, 服務器將根據項 argv[0]的值, 在命令表中查找命令所對應的命令實現函數。
  該表是一個字典, 字典的鍵是一個 SDS結構, 保存了命令的名字, 字典的值是命令所對應的redisCommand結構, 這個結構保存了命令的實現函數、 命令的標誌、 命令應該給定的參數個數、 命令的總執行次數和總消耗時長等統計信息。

  當程序在命令表中成功找到 argv[0]所對應的 redisCommand結構時, 它會將客戶端狀態的 cmd指針指向這個結構。之後, 服務器就可以使用cmd屬性所指向的 redisCommand結構, 以及 argvargc屬性中保存的命令參數信息, 調用命令實現函數, 執行客戶端指定的命令。

struct redisCommand {
    char *name;
    redisCommandProc *proc;
    int arity;
    char *sflags; /* Flags as string representation, one char per flag. */
    int flags;    /* The actual flags, obtained from the 'sflags' field. */
    /* Use a function to determine keys arguments in a command line.
     * Used for Redis Cluster redirect. */
    redisGetKeysProc *getkeys_proc;
    /* What keys should be loaded in background when calling this command? */
    int firstkey; /* The first argument that's a key (0 = no keys) */
    int lastkey;  /* The last argument that's a key */
    int keystep;  /* The step between first and last key */
    long long microseconds, calls;
};

2.5.7 輸出緩衝區

  執行命令所得的命令回覆會被保存在客戶端狀態的輸出緩衝區裏面, 每個客戶端都有兩個輸出緩衝區可用, 一個緩衝區的大小是固定的, 另一個緩衝區的大小是可變的:

  • 固定大小的緩衝區用於保存那些長度比較小的回覆, 比如 OK 、簡短的字符串值、整數值、錯誤回覆,等等。大小爲PROTO_REPLY_CHUNK_BYTES,是16KB;
  • 可變大小的緩衝區用於保存那些長度比較大的回覆, 比如一個非常長的字符串值, 一個由很多項組成的列表, 一個包含了很多元素的集合, 等等。可變大小緩衝區由 reply 鏈表和一個或多個字符串對象組成。
      客戶端的固定大小緩衝區由 buf 和 bufpos 兩個屬性組成。

2.5.8 身份驗證

  客戶端狀態的 authenticated 屬性用於記錄客戶端是否通過了身份驗證:

  • 0,未通過;
  • 1,通過。

2.5.9 時間

  和時間相關的三個屬性如下:

    time_t ctime;           /* Client creation time. */
    time_t lastinteraction; /* Time of the last interaction, used for timeout */
    time_t obuf_soft_limit_reached_time;
  • ctime:表示客戶端創建時間;
  • lastinteraction:標示最後一次進行互動的時間;
  • obuf_soft_limit_reached_time:輸出緩衝區第一次到達軟性限制(soft limit)的時間。

2.6 服務器

  redis中客戶端發送一個命令完成一系列操作的基本過程如下:

  1. 客戶端向服務端發送命令請求;
  2. 服務端接受客戶端的命令並記性處理,產生返回碼;
  3. 服務端將返回碼發送給客戶端;
  4. 客戶端接受服務器的命令返回碼,並輸出到輸出流。

2.6.1 發送命令

  發送命令的基本過程爲:用戶----鍵入命令—>客戶端-----將命令轉換成協議格式發送----->服務端。

2.6.2 讀取命令請求

  當客戶端與服務器之間的連接套接字因爲客戶端的寫入而變得可讀時, 服務器將調用命令請求處理器來執行以下操作:

  1. 讀取套接字中協議格式的命令請求, 並將其保存到客戶端狀態的輸入緩衝區裏面;
  2. 對輸入緩衝區中的命令請求進行分析, 提取出命令請求中包含的命令參數, 以及命令參數的個數, 然後分別將參數和參數個數保存到客戶端狀態的 argv 屬性和 argc 屬性裏面;
  3. 調用命令執行器, 執行客戶端指定的命令。

2.6.3 執行命令

2.6.3.1 查找命令實現

  命令執行器要做的第一件事就是根據客戶端狀態的 argv[0]參數, 在命令表(command table)中查找參數所指定的命令, 並將找到的命令保存到客戶端狀態的 cmd屬性裏面。
  命令表是一個字典, 字典的鍵是一個個命令名字,比如 "set" 、 "get" 、 "del"等等; 而字典的值則是一個個 redisCommand結構, 每個redisCommand結構記錄了一個 Redis 命令的實現信息。

struct redisCommand {
    char *name;
    redisCommandProc *proc;
    int arity;
    char *sflags; /* Flags as string representation, one char per flag. */
    int flags;    /* The actual flags, obtained from the 'sflags' field. */
    /* Use a function to determine keys arguments in a command line.
     * Used for Redis Cluster redirect. */
    redisGetKeysProc *getkeys_proc;
    /* What keys should be loaded in background when calling this command? */
    int firstkey; /* The first argument that's a key (0 = no keys) */
    int lastkey;  /* The last argument that's a key */
    int keystep;  /* The step between first and last key */
    long long microseconds, calls;
};
  • name:命令的名字;
  • proc:命令需要執行的函數,即typedef void redisCommandProc(client *c);
  • arity:命令參數的個數,用於檢查命令請求的格式是否正確。 如果這個值爲負數 -N ,那麼表示參數的數量大於等於 N 。 注意命令的名字本身也是一個參數;
  • sflags:字符串形式的標識值, 這個值記錄了命令的屬性, 比如這個命令是寫命令還是讀命令, 這個命令是否允許在載入數據時使用, 這個命令是否允許在 Lua 腳本中使用, 等等;
  • flags:對 sflags 標識進行分析得出的二進制標識, 由程序自動生成。 服務器對命令標識進行檢查時使用的都是 flags屬性而不是 sflags屬性;
  • microseconds:服務器執行這個命令所耗費的總時長;
  • calls:服務器總共執行了多少次這個命令。

  sflags屬性標識:

標識 意義 帶有這個標識的命令
w 這是一個寫入命令,可能會修改數據庫。 SET 、 RPUSH 、 DEL,等等。
r 這是一個只讀命令,不會修改數據庫。 GET 、 STRLEN 、 EXISTS,等等。
m 這個命令可能會佔用大量內存, 執行之前需要先檢查服務器的內存使用情況, 如果內存緊缺的話就禁止執行這個命令。 SET 、 APPEND 、 RPUSH 、 LPUSH 、 SADD 、SINTERSTORE,等等。
a 這是一個管理命令。 SAVE 、 BGSAVE 、 SHUTDOWN,等等。
p 這是一個發佈與訂閱功能方面的命令。 PUBLISH 、 SUBSCRIBE 、 PUBSUB,等等。
s 這個命令不可以在 Lua 腳本中使用。 BRPOP 、 BLPOP 、 BRPOPLPUSH 、 SPOP,等等。
R 這是一個隨機命令, 對於相同的數據集和相同的參數, 命令返回的結果可能不同。 SPOP 、 SRANDMEMBER 、 SSCAN 、 RANDOMKEY,等等。
S 當在 Lua 腳本中使用這個命令時, 對這個命令的輸出結果進行一次排序, 使得命令的結果有序。 SINTER 、 SUNION 、 SDIFF 、 SMEMBERS 、KEYS,等等。
l 這個命令可以在服務器載入數據的過程中使用。 INFO 、 SHUTDOWN 、 PUBLISH,等等。
t 這是一個允許從服務器在帶有過期數據時使用的命令。 SLAVEOF 、 PING 、 INFO,等等。
M 這個命令在監視器(monitor)模式下不會自動被傳播(propagate)。 EXEC

2.6.3.2 執行預備操作

  服務器已經將執行命令所需的命令實現函數(保存在客戶端狀態的 cmd 屬性)、參數(保存在客戶端狀態的 argv屬性)、參數個數(保存在客戶端狀態的 argc 屬性)都收集齊了, 但是在真正執行命令之前, 程序還需要進行一些預備操作, 從而確保命令可以正確、順利地被執行, 這些操作包括:

  • 檢查客戶端狀態的cmd指針是否指向 NULL, 如果是的話, 那麼說明用戶輸入的命令名字找不到相應的命令實現, 服務器不再執行後續步驟, 並向客戶端返回一個錯誤;
  • 根據客戶端 cmd屬性指向的 redisCommand結構的 arity屬性, 檢查命令請求所給定的參數個數是否正確, 當參數個數不正確時, 不再執行後續步驟, 直接向客戶端返回一個錯誤。 比如說, 如果 redisCommand結構的arity屬性的值爲 -3 , 那麼用戶輸入的命令參數個數必須大於等於 3 個纔行;
  • 檢查客戶端是否已經通過了身份驗證, 未通過身份驗證的客戶端只能執行 AUTH命令, 如果未通過身份驗證的客戶端試圖執行除 AUTH命令之外的其他命令, 那麼服務器將向客戶端返回一個錯誤;
  • 如果服務器打開了 maxmemory 功能, 那麼在執行命令之前, 先檢查服務器的內存佔用情況, 並在有需要時進行內存回收, 從而使得接下來的命令可以順利執行。 如果內存回收失敗, 那麼不再執行後續步驟, 向客戶端返回一個錯誤;
  • 如果服務器上一次執行 BGSAVE 命令時出錯, 並且服務器打開了 stop-writes-on-bgsave-error 功能, 而且服務器即將要執行的命令是一個寫命令, 那麼服務器將拒絕執行這個命令, 並向客戶端返回一個錯誤;
  • 如果客戶端當前正在用SUBSCRIBE命令訂閱頻道, 或者正在用 PSUBSCRIBE命令訂閱模式, 那麼服務器只會執行客戶端發來的SUBSCRIBE 、 PSUBSCRIBE 、 UNSUBSCRIBE 、 PUNSUBSCRIBE四個命令, 其他別的命令都會被服務器拒絕;
  • 如果服務器正在進行數據載入, 那麼客戶端發送的命令必須帶有 l 標識(比如 INFO 、 SHUTDOWN 、 PUBLISH,等等)纔會被服務器執行, 其他別的命令都會被服務器拒絕;
  • 如果服務器因爲執行 Lua 腳本而超時並進入阻塞狀態, 那麼服務器只會執行客戶端發來的 SHUTDOWN nosave命令和SCRIPT KILL命令, 其他別的命令都會被服務器拒絕;
  • 如果客戶端正在執行事務, 那麼服務器只會執行客戶端發來的 EXEC 、 DISCARD 、 MULTI 、 WATCH四個命令, 其他命令都會被放進事務隊列中;
  • 如果服務器打開了監視器功能, 那麼服務器會將要執行的命令和參數等信息發送給監視器。

2.6.3.3 調用命令的實現函數

  服務器已經將要執行命令的實現保存到了客戶端狀態的 cmd 屬性裏面, 並將命令的參數和參數個數分別保存到了客戶端狀態的 argv 屬性和 argc 屬性裏面, 當服務器決定要執行命令時, 它只要執行以下語句:

client->cmd->proc(client);

2.6.3.4 執行後續工作

  在執行完實現函數之後, 服務器還需要執行一些後續工作:

  • 如果服務器開啓了慢查詢日誌功能, 那麼慢查詢日誌模塊會檢查是否需要爲剛剛執行完的命令請求添加一條新的慢查詢日誌;
  • 根據剛剛執行命令所耗費的時長, 更新被執行命令的 redisCommand結構的 milliseconds屬性, 並將命令的 redisCommand結構的 calls計數器的值增一;
  • 如果服務器開啓了 AOF持久化功能, 那麼 AOF 持久化模塊會將剛剛執行的命令請求寫入到 AOF緩衝區裏面;
  • 如果有其他從服務器正在複製當前這個服務器, 那麼服務器會將剛剛執行的命令傳播給所有從服務器。

2.6.4 發送返回碼並輸出

  命令實現函數會將命令回覆保存到客戶端的輸出緩衝區裏面, 併爲客戶端的套接字關聯命令回覆處理器, 當客戶端套接字變爲可寫狀態時, 服務器就會執行命令回覆處理器, 將保存在客戶端輸出緩衝區中的命令回覆發送給客戶端。
  服務端—返回碼轉換成協議格式—>客戶端—轉成可讀格式輸出—>用戶。

3 多機數據庫的實現

3.1 複製

3.1.1 舊版本實現

  Redis 的複製功能分爲同步(sync)和命令傳播(command propagate)兩個操作:

  • 同步操作用於將從服務器的數據庫狀態更新至主服務器當前所處的數據庫狀態;
  • 命令傳播操作則用於在主服務器的數據庫狀態被修改, 導致主從服務器的數據庫狀態出現不一致時, 讓主從服務器的數據庫重新回到一致狀態。

3.1.1.1 同步

  當客戶端向從服務器發送 SLAVEOF命令, 要求從服務器複製主服務器時, 從服務器首先需要執行同步操作, 也即是, 將從服務器的數據庫狀態更新至主服務器當前所處的數據庫狀態。
  從服務器對主服務器的同步操作需要通過向主服務器發送 SYNC命令來完成。步驟爲:

  1. 從服務器向主服務器發送SYNC
  2. 收到 SYNC 命令的主服務器執行 BGSAVE命令, 在後臺生成一個 RDB文件, 並使用一個緩衝區記錄從現在開始執行的所有寫命令;
  3. 當主服務器的 BGSAVE命令執行完畢時, 主服務器會將 BGSAVE 命令生成的 RDB文件發送給從服務器, 從服務器接收並載入這個 RDB文件, 將自己的數據庫狀態更新至主服務器執行 BGSAVE 命令時的數據庫狀態;
  4. 主服務器將記錄在緩衝區裏面的所有寫命令發送給從服務器, 從服務器執行這些寫命令, 將自己的數據庫狀態更新至主服務器數據庫當前所處的狀態。

3.1.1.2 命令傳播

  在同步操作執行完畢之後, 主從服務器兩者的數據庫將達到一致狀態, 但這種一致並不是一成不變的 —— 每當主服務器執行客戶端發送的寫命令時, 主服務器的數據庫就有可能會被修改, 並導致主從服務器狀態不再一致。
  爲了讓主從服務器再次回到一致狀態, 主服務器需要對從服務器執行命令傳播操作: 主服務器會將自己執行的寫命令 —— 也即是造成主從服務器不一致的那條寫命令 —— 發送給從服務器執行, 當從服務器執行了相同的寫命令之後, 主從服務器將再次回到一致狀態。

3.1.1.3 缺陷

  SYNC命令是一個非常耗費資源的操作
  每次執行SYNC命令,主從服務器需要執行以下動作:

  • 主服務器需要執行BGSAVE命令來生成RDB文件,這個生成操作會耗費主服務器大量的CPU、內存和磁盤I/O資源。
  • 主服務器需要將自己生成的RDB文件發送給從服務器,這個發送操作會耗費主從服務器大量的網絡資源(帶寬和流量),並對主服務器響應命令請求的時間產生影響。
  • 接收到RDB文件的從服務器需要載入主服務器發來的RDB文件,並且在載入期間,從服務器會因爲阻塞而沒辦法處理命令請求。
      因爲SYNC命令是一個如此耗費資源的操作,所以Redis有必要保證在真正有需要時才執行SYNC命令。

  從服務器對主服務器的複製可以分爲以下兩種情況:

  • 初次複製:從服務器以前沒有複製過任何主服務器,或者從服務器當前要複製的主服務器和上一次複製的主服務器不同。
  • 斷線後重複製:處於命令傳播階段的主從服務器因爲網絡原因而中斷了複製,但從服務器通過自動重連接重新連上了主服務器,並繼續複製主服務器。

  對於初次複製來說,舊版複製功能能夠很好地完成任務,但對於斷線後重複製來說,舊版複製功能雖然也能讓主從服務器重新回到一致狀態,但效率卻非常低,從服務器需要讓主服務器將所有執行的寫命令的RDB文件,從新發送給從服務器。但主從服務器斷開的時間可能很短,主服務器在斷線期間執行的寫命令可能很少,而執行少量寫命令所產生的數據量通常比整個數據庫的數據量要少得多,在這種情況下,爲了讓從服務器補足一小部分缺失的數據,卻要讓主從服務器重新執行一次SYNC命令,這種做法無疑是非常低效的。

3.1.2 新版實現

  爲了解決舊版複製功能在處理斷線重複制情況時的低效問題,,Redis從2.8版本開始,使用PSYNC命令代替SYNC命令來執行復制時的同步操作。
  PSYNC命令具有完整重同步( full resynchronization)和部分重同步( partial resynchronization)兩種模式,

  • 其中完整重同步用於處理初次複製情況:完整重同步的執行步驟和SYNC命令的執行步驟基本一樣,它們都是通過讓主服務器創建併發送RDB文件,以及向從服務器發送保存在緩衝區裏面的寫命令來進行同步;
  • 部分重同步則用於處理斷線後重複製情況:當從服務器在斷線後重新連接主服務器時,如果條件允許,主服務器可以將主從服務器連接斷開期間執行的寫命令發送給從服務器,從服務器只要接收並執行這些寫命令,就可以將數據庫更新至主服務器當前所處的狀態。
      PSYNC命令的部分重同步模式解決了舊版複製功能在處理斷線後重複製時出現的低效情況。

3.1.2.1 部分同步的實現

  部分重同步功能由以下三個部分構成:

  • 主服務器的複製偏移量( replication offset )和從服務器的複製偏移量;
  • 主服務器的複製積壓緩衝區(replication backlog);
  • 服務器的運行ID (run ID )。

  複製偏移量:
  執行復制的雙方——主服務器和從服務器會分別維護一個複製偏移量:

  • 主服務器每次向從服務器傳播N個字節的數據時,就將自己的複製偏移量的值加上N;
  • 從服務器每次收到主服務器傳播來的N個字節的數據時,就將自己的複製偏移量的值加上N。

  通過對比主從服務器的複製偏移量,程序可以很容易地知道主從服務器是否處於一致狀態:

  • 如果主從服務器處於一致狀態,那麼主從服務器兩者的偏移量總是相同的;
  • 相反,如果主從服務器兩者的偏移量並不相同,那麼說明主從服務器並未處於一致狀態。

  複製積壓緩衝區:
  複製積壓緩衝區是由主服務器維護的一個固定長度(fixed-size )先進先出( FIFO )隊列,默認大小爲1MB。
  當主服務器進行命令傳播時,它不僅會將寫命令發送給所有從服務器 還會將寫命令人 隊到複製積壓緩衝區裏面 。
  因此,主服務器的複製積壓緩衝區裏面會保存着一部分最近傳播的寫命令,並且複製積壓緩衝區會爲隊列中的每個字節記錄相應的複製偏移量 。
  當從服務器重新連上主服務器時,從服務器會通過PSYNC命令將自己的複製偏移量offset發送給主服務器,主服務器會根據這個複製偏移量來決定對從服務器執行何種同步操作:

  • 如果offset偏移量之後的數據(也即是偏移量offset+1開始的數據)仍然存在於複製積壓緩衝區裏面、那麼主服務器將對從服務器執行部分重同步操作;
  • 相反,如果offset偏移量之後的數據已經不存在於複製積壓緩衝區,那麼主服務器將對從服務器執行完整重同步操作。

  服務器運行ID:
  除了複製偏移量和複製積壓穿衝區之外,實現部分重同步還需要用到服務器運行ID:

  • 每個Redis服務器,不論主服務器還是從服務,都會有自己的運行ID;
  • 運行ID在服務器啓動時自動生成,由40個隨機的十六進制字符組成。
      當從服務器對主服務器進行初次複製時,主服務器會將自己的運行ID傳送給從服務器,而從服務器則會將這個運行ID保存起來。

  當從服務器斷線並重新連上一個主服務器時,從服務器將向當前連接的主服務器發送之前保存的運行ID:

  • 如果從服務器保存的運行ID和當前連接的主服務器的運行ID相同,那麼說明從服務器斷線之前複製的就是當前連接的這個主服務器,主服務器可以繼續嘗試執行部分重同步操作;
  • 相反地,如果從服務器保存的運行ID和當前連接的主服務器的運行ID並不相同,那麼說明從服務器斷線之前複製的主服務器並不是當前連接的這個主服務器,主服務器將對從服務器執行完整重同步操作。

3.2 Sentinel

  redis-sentinel是Redis官方推薦的高可用性(HA)解決方案,當用Redis做Master-slave的高可用方案時,假如master宕機了,Redis本身(包括它的很多客戶端)都沒有實現自動進行主備切換,而Redis-sentinel本身也是一個獨立運行的進程,它能監控多個master-slave集羣,發現master宕機後能進行自懂切換。

  它的主要功能有以下幾點:

  • 不時地監控redis是否按照預期良好地運行;
  • 如果發現某個redis節點運行出現狀況,能夠通知另外一個進程(例如它的客戶端);
  • 能夠進行自動切換。當一個master節點不可用時,能夠選舉出master的多個slave(如果有超過一個slave的話)中的一個來作爲新的master,其它的slave節點會將它所追隨的master的地址改爲被提升爲master的slave的新地址。

  啓動sentinel可以使用redis-sentinel /path/to/your/sentinel.conf或者redis-server /path/to/your/sentinel.conf --sentinel。啓動一個sentinel時,它需要執行以下步驟:

  1. 初始化服務器;
  2. 將普通 Redis 服務器使用的代碼替換成 Sentinel 專用代碼;
  3. 初始化 Sentinel 狀態;
  4. 根據給定的配置文件, 初始化 Sentinel 的監視主服務器列表;
  5. 創建連向主服務器的網絡連接。

3.2.1 初始化服務器

  因爲 Sentinel 本質上只是一個運行在特殊模式下的 Redis 服務器, 所以啓動 Sentinel 的第一步, 就是初始化一個普通的 Redis 服務器。但是Sentinel 執行的工作和普通 Redis 服務器執行的工作不同, 所以 Sentinel 的初始化過程和普通 Redis 服務器的初始化過程並不完全相同。
  下表爲Sentinel 模式下 Redis 服務器主要功能的使用情況:

功能 使用情況
數據庫和鍵值對方面的命令, 比如 SET 、 DEL 、FLUSHDB 。 不使用。
事務命令, 比如 MULTI 和 WATCH 。 不使用。
腳本命令,比如 EVAL 。 不使用。
RDB 持久化命令, 比如 SAVE 和 BGSAVE 。 不使用。
AOF 持久化命令, 比如 BGREWRITEAOF 。 不使用。
複製命令,比如 SLAVEOF 。 Sentinel 內部可以使用,但客戶端不可以使用。
發佈與訂閱命令, 比如 PUBLISH 和 SUBSCRIBE 。 SUBSCRIBE 、 PSUBSCRIBE 、 UNSUBSCRIBE PUNSUBSCRIBE 四個命令在 Sentinel 內部和客戶端都可以使用, 但 PUBLISH 命令只能在 Sentinel 內部使用。
文件事件處理器(負責發送命令請求、處理命令回覆)。 Sentinel 內部使用, 但關聯的文件事件處理器和普通 Redis 服務器不同。
時間事件處理器(負責執行 serverCron 函數)。 Sentinel 內部使用, 時間事件的處理器仍然是 serverCron 函數, serverCron函數會調用 sentinel.c/sentinelTimer 函數, 後者包含了 Sentinel 要執行的所有操作。

3.2.2 使用 Sentinel 專用代碼

struct redisCommand redisCommandTable[] = {
    {"module",moduleCommand,-2,"as",0,NULL,0,0,0,0,0},
    {"get",getCommand,2,"rF",0,NULL,1,1,1,0,0},
    {"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0},
    {"setnx",setnxCommand,3,"wmF",0,NULL,1,1,1,0,0},
    {"setex",setexCommand,4,"wm",0,NULL,1,1,1,0,0},
    {"psetex",psetexCommand,4,"wm",0,NULL,1,1,1,0,0},
    {"append",appendCommand,3,"wm",0,NULL,1,1,1,0,0},
    {"strlen",strlenCommand,2,"rF",0,NULL,1,1,1,0,0},
    {"del",delCommand,-2,"w",0,NULL,1,-1,1,0,0},
    {"unlink",unlinkCommand,-2,"wF",0,NULL,1,-1,1,0,0},
    {"exists",existsCommand,-2,"rF",0,NULL,1,-1,1,0,0},
    {"setbit",setbitCommand,4,"wm",0,NULL,1,1,1,0,0},
    {"getbit",getbitCommand,3,"rF",0,NULL,1,1,1,0,0},
    {"bitfield",bitfieldCommand,-2,"wm",0,NULL,1,1,1,0,0},
    {"setrange",setrangeCommand,4,"wm",0,NULL,1,1,1,0,0},
    {"getrange",getrangeCommand,4,"r",0,NULL,1,1,1,0,0},
    {"substr",getrangeCommand,4,"r",0,NULL,1,1,1,0,0},
    {"incr",incrCommand,2,"wmF",0,NULL,1,1,1,0,0},
    {"decr",decrCommand,2,"wmF",0,NULL,1,1,1,0,0},
    {"mget",mgetCommand,-2,"rF",0,NULL,1,-1,1,0,0},
    {"rpush",rpushCommand,-3,"wmF",0,NULL,1,1,1,0,0},
    {"lpush",lpushCommand,-3,"wmF",0,NULL,1,1,1,0,0},
    {"rpushx",rpushxCommand,-3,"wmF",0,NULL,1,1,1,0,0},
    {"lpushx",lpushxCommand,-3,"wmF",0,NULL,1,1,1,0,0},
    {"linsert",linsertCommand,5,"wm",0,NULL,1,1,1,0,0},
    {"rpop",rpopCommand,2,"wF",0,NULL,1,1,1,0,0},
    {"lpop",lpopCommand,2,"wF",0,NULL,1,1,1,0,0},
    {"brpop",brpopCommand,-3,"ws",0,NULL,1,-2,1,0,0},
    {"brpoplpush",brpoplpushCommand,4,"wms",0,NULL,1,2,1,0,0},
    {"blpop",blpopCommand,-3,"ws",0,NULL,1,-2,1,0,0},
    {"llen",llenCommand,2,"rF",0,NULL,1,1,1,0,0},
    {"lindex",lindexCommand,3,"r",0,NULL,1,1,1,0,0},
    {"lset",lsetCommand,4,"wm",0,NULL,1,1,1,0,0},
    {"lrange",lrangeCommand,4,"r",0,NULL,1,1,1,0,0},
    {"ltrim",ltrimCommand,4,"w",0,NULL,1,1,1,0,0},
    {"lrem",lremCommand,4,"w",0,NULL,1,1,1,0,0},
    {"rpoplpush",rpoplpushCommand,3,"wm",0,NULL,1,2,1,0,0},
    {"sadd",saddCommand,-3,"wmF",0,NULL,1,1,1,0,0},
    {"srem",sremCommand,-3,"wF",0,NULL,1,1,1,0,0},
    {"smove",smoveCommand,4,"wF",0,NULL,1,2,1,0,0},
    {"sismember",sismemberCommand,3,"rF",0,NULL,1,1,1,0,0},
    {"scard",scardCommand,2,"rF",0,NULL,1,1,1,0,0},
    {"spop",spopCommand,-2,"wRF",0,NULL,1,1,1,0,0},
    {"srandmember",srandmemberCommand,-2,"rR",0,NULL,1,1,1,0,0},
    {"sinter",sinterCommand,-2,"rS",0,NULL,1,-1,1,0,0},
    {"sinterstore",sinterstoreCommand,-3,"wm",0,NULL,1,-1,1,0,0},
    {"sunion",sunionCommand,-2,"rS",0,NULL,1,-1,1,0,0},
    {"sunionstore",sunionstoreCommand,-3,"wm",0,NULL,1,-1,1,0,0},
    {"sdiff",sdiffCommand,-2,"rS",0,NULL,1,-1,1,0,0},
    {"sdiffstore",sdiffstoreCommand,-3,"wm",0,NULL,1,-1,1,0,0},
    {"smembers",sinterCommand,2,"rS",0,NULL,1,1,1,0,0},
    {"sscan",sscanCommand,-3,"rR",0,NULL,1,1,1,0,0},
    {"zadd",zaddCommand,-4,"wmF",0,NULL,1,1,1,0,0},
    {"zincrby",zincrbyCommand,4,"wmF",0,NULL,1,1,1,0,0},
    {"zrem",zremCommand,-3,"wF",0,NULL,1,1,1,0,0},
    {"zremrangebyscore",zremrangebyscoreCommand,4,"w",0,NULL,1,1,1,0,0},
    {"zremrangebyrank",zremrangebyrankCommand,4,"w",0,NULL,1,1,1,0,0},
    {"zremrangebylex",zremrangebylexCommand,4,"w",0,NULL,1,1,1,0,0},
    {"zunionstore",zunionstoreCommand,-4,"wm",0,zunionInterGetKeys,0,0,0,0,0},
    {"zinterstore",zinterstoreCommand,-4,"wm",0,zunionInterGetKeys,0,0,0,0,0},
    {"zrange",zrangeCommand,-4,"r",0,NULL,1,1,1,0,0},
    {"zrangebyscore",zrangebyscoreCommand,-4,"r",0,NULL,1,1,1,0,0},
    {"zrevrangebyscore",zrevrangebyscoreCommand,-4,"r",0,NULL,1,1,1,0,0},
    {"zrangebylex",zrangebylexCommand,-4,"r",0,NULL,1,1,1,0,0},
    {"zrevrangebylex",zrevrangebylexCommand,-4,"r",0,NULL,1,1,1,0,0},
    {"zcount",zcountCommand,4,"rF",0,NULL,1,1,1,0,0},
    {"zlexcount",zlexcountCommand,4,"rF",0,NULL,1,1,1,0,0},
    {"zrevrange",zrevrangeCommand,-4,"r",0,NULL,1,1,1,0,0},
    {"zcard",zcardCommand,2,"rF",0,NULL,1,1,1,0,0},
    {"zscore",zscoreCommand,3,"rF",0,NULL,1,1,1,0,0},
    {"zrank",zrankCommand,3,"rF",0,NULL,1,1,1,0,0},
    {"zrevrank",zrevrankCommand,3,"rF",0,NULL,1,1,1,0,0},
    {"zscan",zscanCommand,-3,"rR",0,NULL,1,1,1,0,0},
    {"zpopmin",zpopminCommand,-2,"wF",0,NULL,1,1,1,0,0},
    {"zpopmax",zpopmaxCommand,-2,"wF",0,NULL,1,1,1,0,0},
    {"bzpopmin",bzpopminCommand,-3,"wsF",0,NULL,1,-2,1,0,0},
    {"bzpopmax",bzpopmaxCommand,-3,"wsF",0,NULL,1,-2,1,0,0},
    {"hset",hsetCommand,-4,"wmF",0,NULL,1,1,1,0,0},
    {"hsetnx",hsetnxCommand,4,"wmF",0,NULL,1,1,1,0,0},
    {"hget",hgetCommand,3,"rF",0,NULL,1,1,1,0,0},
    {"hmset",hsetCommand,-4,"wmF",0,NULL,1,1,1,0,0},
    {"hmget",hmgetCommand,-3,"rF",0,NULL,1,1,1,0,0},
    {"hincrby",hincrbyCommand,4,"wmF",0,NULL,1,1,1,0,0},
    {"hincrbyfloat",hincrbyfloatCommand,4,"wmF",0,NULL,1,1,1,0,0},
    {"hdel",hdelCommand,-3,"wF",0,NULL,1,1,1,0,0},
    {"hlen",hlenCommand,2,"rF",0,NULL,1,1,1,0,0},
    {"hstrlen",hstrlenCommand,3,"rF",0,NULL,1,1,1,0,0},
    {"hkeys",hkeysCommand,2,"rS",0,NULL,1,1,1,0,0},
    {"hvals",hvalsCommand,2,"rS",0,NULL,1,1,1,0,0},
    {"hgetall",hgetallCommand,2,"rR",0,NULL,1,1,1,0,0},
    {"hexists",hexistsCommand,3,"rF",0,NULL,1,1,1,0,0},
    {"hscan",hscanCommand,-3,"rR",0,NULL,1,1,1,0,0},
    {"incrby",incrbyCommand,3,"wmF",0,NULL,1,1,1,0,0},
    {"decrby",decrbyCommand,3,"wmF",0,NULL,1,1,1,0,0},
    {"incrbyfloat",incrbyfloatCommand,3,"wmF",0,NULL,1,1,1,0,0},
    {"getset",getsetCommand,3,"wm",0,NULL,1,1,1,0,0},
    {"mset",msetCommand,-3,"wm",0,NULL,1,-1,2,0,0},
    {"msetnx",msetnxCommand,-3,"wm",0,NULL,1,-1,2,0,0},
    {"randomkey",randomkeyCommand,1,"rR",0,NULL,0,0,0,0,0},
    {"select",selectCommand,2,"lF",0,NULL,0,0,0,0,0},
    {"swapdb",swapdbCommand,3,"wF",0,NULL,0,0,0,0,0},
    {"move",moveCommand,3,"wF",0,NULL,1,1,1,0,0},
    {"rename",renameCommand,3,"w",0,NULL,1,2,1,0,0},
    {"renamenx",renamenxCommand,3,"wF",0,NULL,1,2,1,0,0},
    {"expire",expireCommand,3,"wF",0,NULL,1,1,1,0,0},
    {"expireat",expireatCommand,3,"wF",0,NULL,1,1,1,0,0},
    {"pexpire",pexpireCommand,3,"wF",0,NULL,1,1,1,0,0},
    {"pexpireat",pexpireatCommand,3,"wF",0,NULL,1,1,1,0,0},
    {"keys",keysCommand,2,"rS",0,NULL,0,0,0,0,0},
    {"scan",scanCommand,-2,"rR",0,NULL,0,0,0,0,0},
    {"dbsize",dbsizeCommand,1,"rF",0,NULL,0,0,0,0,0},
    {"auth",authCommand,2,"sltF",0,NULL,0,0,0,0,0},
    {"ping",pingCommand,-1,"tF",0,NULL,0,0,0,0,0},
    {"echo",echoCommand,2,"F",0,NULL,0,0,0,0,0},
    {"save",saveCommand,1,"as",0,NULL,0,0,0,0,0},
    {"bgsave",bgsaveCommand,-1,"as",0,NULL,0,0,0,0,0},
    {"bgrewriteaof",bgrewriteaofCommand,1,"as",0,NULL,0,0,0,0,0},
    {"shutdown",shutdownCommand,-1,"aslt",0,NULL,0,0,0,0,0},
    {"lastsave",lastsaveCommand,1,"RF",0,NULL,0,0,0,0,0},
    {"type",typeCommand,2,"rF",0,NULL,1,1,1,0,0},
    {"multi",multiCommand,1,"sF",0,NULL,0,0,0,0,0},
    {"exec",execCommand,1,"sM",0,NULL,0,0,0,0,0},
    {"discard",discardCommand,1,"sF",0,NULL,0,0,0,0,0},
    {"sync",syncCommand,1,"ars",0,NULL,0,0,0,0,0},
    {"psync",syncCommand,3,"ars",0,NULL,0,0,0,0,0},
    {"replconf",replconfCommand,-1,"aslt",0,NULL,0,0,0,0,0},
    {"flushdb",flushdbCommand,-1,"w",0,NULL,0,0,0,0,0},
    {"flushall",flushallCommand,-1,"w",0,NULL,0,0,0,0,0},
    {"sort",sortCommand,-2,"wm",0,sortGetKeys,1,1,1,0,0},
    {"info",infoCommand,-1,"ltR",0,NULL,0,0,0,0,0},
    {"monitor",monitorCommand,1,"as",0,NULL,0,0,0,0,0},
    {"ttl",ttlCommand,2,"rFR",0,NULL,1,1,1,0,0},
    {"touch",touchCommand,-2,"rF",0,NULL,1,1,1,0,0},
    {"pttl",pttlCommand,2,"rFR",0,NULL,1,1,1,0,0},
    {"persist",persistCommand,2,"wF",0,NULL,1,1,1,0,0},
    {"slaveof",replicaofCommand,3,"ast",0,NULL,0,0,0,0,0},
    {"replicaof",replicaofCommand,3,"ast",0,NULL,0,0,0,0,0},
    {"role",roleCommand,1,"lst",0,NULL,0,0,0,0,0},
    {"debug",debugCommand,-2,"as",0,NULL,0,0,0,0,0},
    {"config",configCommand,-2,"last",0,NULL,0,0,0,0,0},
    {"subscribe",subscribeCommand,-2,"pslt",0,NULL,0,0,0,0,0},
    {"unsubscribe",unsubscribeCommand,-1,"pslt",0,NULL,0,0,0,0,0},
    {"psubscribe",psubscribeCommand,-2,"pslt",0,NULL,0,0,0,0,0},
    {"punsubscribe",punsubscribeCommand,-1,"pslt",0,NULL,0,0,0,0,0},
    {"publish",publishCommand,3,"pltF",0,NULL,0,0,0,0,0},
    {"pubsub",pubsubCommand,-2,"pltR",0,NULL,0,0,0,0,0},
    {"watch",watchCommand,-2,"sF",0,NULL,1,-1,1,0,0},
    {"unwatch",unwatchCommand,1,"sF",0,NULL,0,0,0,0,0},
    {"cluster",clusterCommand,-2,"a",0,NULL,0,0,0,0,0},
    {"restore",restoreCommand,-4,"wm",0,NULL,1,1,1,0,0},
    {"restore-asking",restoreCommand,-4,"wmk",0,NULL,1,1,1,0,0},
    {"migrate",migrateCommand,-6,"wR",0,migrateGetKeys,0,0,0,0,0},
    {"asking",askingCommand,1,"F",0,NULL,0,0,0,0,0},
    {"readonly",readonlyCommand,1,"F",0,NULL,0,0,0,0,0},
    {"readwrite",readwriteCommand,1,"F",0,NULL,0,0,0,0,0},
    {"dump",dumpCommand,2,"rR",0,NULL,1,1,1,0,0},
    {"object",objectCommand,-2,"rR",0,NULL,2,2,1,0,0},
    {"memory",memoryCommand,-2,"rR",0,NULL,0,0,0,0,0},
    {"client",clientCommand,-2,"as",0,NULL,0,0,0,0,0},
    {"eval",evalCommand,-3,"s",0,evalGetKeys,0,0,0,0,0},
    {"evalsha",evalShaCommand,-3,"s",0,evalGetKeys,0,0,0,0,0},
    {"slowlog",slowlogCommand,-2,"aR",0,NULL,0,0,0,0,0},
    {"script",scriptCommand,-2,"s",0,NULL,0,0,0,0,0},
    {"time",timeCommand,1,"RF",0,NULL,0,0,0,0,0},
    {"bitop",bitopCommand,-4,"wm",0,NULL,2,-1,1,0,0},
    {"bitcount",bitcountCommand,-2,"r",0,NULL,1,1,1,0,0},
    {"bitpos",bitposCommand,-3,"r",0,NULL,1,1,1,0,0},
    {"wait",waitCommand,3,"s",0,NULL,0,0,0,0,0},
    {"command",commandCommand,0,"ltR",0,NULL,0,0,0,0,0},
    {"geoadd",geoaddCommand,-5,"wm",0,NULL,1,1,1,0,0},
    {"georadius",georadiusCommand,-6,"w",0,georadiusGetKeys,1,1,1,0,0},
    {"georadius_ro",georadiusroCommand,-6,"r",0,georadiusGetKeys,1,1,1,0,0},
    {"georadiusbymember",georadiusbymemberCommand,-5,"w",0,georadiusGetKeys,1,1,1,0,0},
    {"georadiusbymember_ro",georadiusbymemberroCommand,-5,"r",0,georadiusGetKeys,1,1,1,0,0},
    {"geohash",geohashCommand,-2,"r",0,NULL,1,1,1,0,0},
    {"geopos",geoposCommand,-2,"r",0,NULL,1,1,1,0,0},
    {"geodist",geodistCommand,-4,"r",0,NULL,1,1,1,0,0},
    {"pfselftest",pfselftestCommand,1,"a",0,NULL,0,0,0,0,0},
    {"pfadd",pfaddCommand,-2,"wmF",0,NULL,1,1,1,0,0},
    {"pfcount",pfcountCommand,-2,"r",0,NULL,1,-1,1,0,0},
    {"pfmerge",pfmergeCommand,-2,"wm",0,NULL,1,-1,1,0,0},
    {"pfdebug",pfdebugCommand,-3,"w",0,NULL,0,0,0,0,0},
    {"xadd",xaddCommand,-5,"wmFR",0,NULL,1,1,1,0,0},
    {"xrange",xrangeCommand,-4,"r",0,NULL,1,1,1,0,0},
    {"xrevrange",xrevrangeCommand,-4,"r",0,NULL,1,1,1,0,0},
    {"xlen",xlenCommand,2,"rF",0,NULL,1,1,1,0,0},
    {"xread",xreadCommand,-4,"rs",0,xreadGetKeys,1,1,1,0,0},
    {"xreadgroup",xreadCommand,-7,"ws",0,xreadGetKeys,1,1,1,0,0},
    {"xgroup",xgroupCommand,-2,"wm",0,NULL,2,2,1,0,0},
    {"xsetid",xsetidCommand,3,"wmF",0,NULL,1,1,1,0,0},
    {"xack",xackCommand,-4,"wF",0,NULL,1,1,1,0,0},
    {"xpending",xpendingCommand,-3,"rR",0,NULL,1,1,1,0,0},
    {"xclaim",xclaimCommand,-6,"wRF",0,NULL,1,1,1,0,0},
    {"xinfo",xinfoCommand,-2,"rR",0,NULL,2,2,1,0,0},
    {"xdel",xdelCommand,-3,"wF",0,NULL,1,1,1,0,0},
    {"xtrim",xtrimCommand,-2,"wFR",0,NULL,1,1,1,0,0},
    {"post",securityWarningCommand,-1,"lt",0,NULL,0,0,0,0,0},
    {"host:",securityWarningCommand,-1,"lt",0,NULL,0,0,0,0,0},
    {"latency",latencyCommand,-2,"aslt",0,NULL,0,0,0,0,0},
    {"lolwut",lolwutCommand,-1,"r",0,NULL,0,0,0,0,0}
};
struct redisCommand sentinelcmds[] = {
    {"ping",pingCommand,1,"",0,NULL,0,0,0,0,0},
    {"sentinel",sentinelCommand,-2,"",0,NULL,0,0,0,0,0},
    {"subscribe",subscribeCommand,-2,"",0,NULL,0,0,0,0,0},
    {"unsubscribe",unsubscribeCommand,-1,"",0,NULL,0,0,0,0,0},
    {"psubscribe",psubscribeCommand,-2,"",0,NULL,0,0,0,0,0},
    {"punsubscribe",punsubscribeCommand,-1,"",0,NULL,0,0,0,0,0},
    {"publish",sentinelPublishCommand,3,"",0,NULL,0,0,0,0,0},
    {"info",sentinelInfoCommand,-1,"",0,NULL,0,0,0,0,0},
    {"role",sentinelRoleCommand,1,"l",0,NULL,0,0,0,0,0},
    {"client",clientCommand,-2,"rs",0,NULL,0,0,0,0,0},
    {"shutdown",shutdownCommand,-1,"",0,NULL,0,0,0,0,0},
    {"auth",authCommand,2,"sltF",0,NULL,0,0,0,0,0}
};

3.2.3 初始化 Sentinel 狀態

  接下來, 服務器會初始化一個 sentinel.c/sentinelState 結構(後面簡稱“Sentinel 狀態”), 這個結構保存了服務器中所有和 Sentinel 功能有關的狀態 (服務器的一般狀態仍然由 redis.h/redisServer 結構保存)。

/* Main state. */
struct sentinelState {
    char myid[CONFIG_RUN_ID_SIZE+1]; /* This sentinel ID. */
    uint64_t current_epoch;         /* Current epoch. */
    dict *masters;      /* Dictionary of master sentinelRedisInstances.
                           Key is the instance name, value is the
                           sentinelRedisInstance structure pointer. */
    int tilt;           /* Are we in TILT mode? */
    int running_scripts;    /* Number of scripts in execution right now. */
    mstime_t tilt_start_time;       /* When TITL started. */
    mstime_t previous_time;         /* Last time we ran the time handler. */
    list *scripts_queue;            /* Queue of user scripts to execute. */
    char *announce_ip;  /* IP addr that is gossiped to other sentinels if
                           not NULL. */
    int announce_port;  /* Port that is gossiped to other sentinels if
                           non zero. */
    unsigned long simfailure_flags; /* Failures simulation. */
    int deny_scripts_reconfig; /* Allow SENTINEL SET ... to change script
                                  paths at runtime? */
} sentinel;

3.2.4 初始化 Sentinel 狀態的 masters 屬性

  Sentinel 狀態中的 masters 字典記錄了所有被 Sentinel 監視的主服務器的相關信息

  • 字典的鍵是被監視主服務器的名字;
  • 字典的值則是被監視主服務器對應的 sentinel.c/sentinelRedisInstance 結構。

  每個 sentinelRedisInstance 結構(後面簡稱“實例結構”)代表一個被 Sentinel 監視的 Redis 服務器實例(instance), 這個實例可以是主服務器、從服務器、或者另外一個 Sentinel 。

typedef struct sentinelRedisInstance {
    int flags;      /* See SRI_... defines */
    char *name;     /* Master name from the point of view of this sentinel. */
    char *runid;    /* Run ID of this instance, or unique ID if is a Sentinel.*/
    uint64_t config_epoch;  /* Configuration epoch. */
    sentinelAddr *addr; /* Master host. */
    instanceLink *link; /* Link to the instance, may be shared for Sentinels. */
    mstime_t last_pub_time;   /* Last time we sent hello via Pub/Sub. */
    mstime_t last_hello_time; /* Only used if SRI_SENTINEL is set. Last time
                                 we received a hello from this Sentinel
                                 via Pub/Sub. */
    mstime_t last_master_down_reply_time; /* Time of last reply to
                                             SENTINEL is-master-down command. */
    mstime_t s_down_since_time; /* Subjectively down since time. */
    mstime_t o_down_since_time; /* Objectively down since time. */
    mstime_t down_after_period; /* Consider it down after that period. */
    mstime_t info_refresh;  /* Time at which we received INFO output from it. */
    dict *renamed_commands;     /* Commands renamed in this instance:
                                   Sentinel will use the alternative commands
                                   mapped on this table to send things like
                                   SLAVEOF, CONFING, INFO, ... */

    /* Role and the first time we observed it.
     * This is useful in order to delay replacing what the instance reports
     * with our own configuration. We need to always wait some time in order
     * to give a chance to the leader to report the new configuration before
     * we do silly things. */
    int role_reported;
    mstime_t role_reported_time;
    mstime_t slave_conf_change_time; /* Last time slave master addr changed. */

    /* Master specific. */
    dict *sentinels;    /* Other sentinels monitoring the same master. */
    dict *slaves;       /* Slaves for this master instance. */
    unsigned int quorum;/* Number of sentinels that need to agree on failure. */
    int parallel_syncs; /* How many slaves to reconfigure at same time. */
    char *auth_pass;    /* Password to use for AUTH against master & slaves. */

    /* Slave specific. */
    mstime_t master_link_down_time; /* Slave replication link down time. */
    int slave_priority; /* Slave priority according to its INFO output. */
    mstime_t slave_reconf_sent_time; /* Time at which we sent SLAVE OF <new> */
    struct sentinelRedisInstance *master; /* Master instance if it's slave. */
    char *slave_master_host;    /* Master host as reported by INFO */
    int slave_master_port;      /* Master port as reported by INFO */
    int slave_master_link_status; /* Master link status as reported by INFO */
    unsigned long long slave_repl_offset; /* Slave replication offset. */
    /* Failover */
    char *leader;       /* If this is a master instance, this is the runid of
                           the Sentinel that should perform the failover. If
                           this is a Sentinel, this is the runid of the Sentinel
                           that this Sentinel voted as leader. */
    uint64_t leader_epoch; /* Epoch of the 'leader' field. */
    uint64_t failover_epoch; /* Epoch of the currently started failover. */
    int failover_state; /* See SENTINEL_FAILOVER_STATE_* defines. */
    mstime_t failover_state_change_time;
    mstime_t failover_start_time;   /* Last failover attempt start time. */
    mstime_t failover_timeout;      /* Max time to refresh failover state. */
    mstime_t failover_delay_logged; /* For what failover_start_time value we
                                       logged the failover delay. */
    struct sentinelRedisInstance *promoted_slave; /* Promoted slave instance. */
    /* Scripts executed to notify admin or reconfigure clients: when they
     * are set to NULL no script is executed. */
    char *notification_script;
    char *client_reconfig_script;
    sds info; /* cached INFO output */
} sentinelRedisInstance;
/* Address object, used to describe an ip:port pair. */
typedef struct sentinelAddr {
    char *ip;
    int port;
} sentinelAddr;

  對 Sentinel 狀態的初始化將引發對 masters 字典的初始化, 而 masters 字典的初始化是根據被載入的 Sentinel 配置文件來進行的。

3.2.5 創建連向主服務器的網絡連接

  初始化 Sentinel 的最後一步是創建連向被監視主服務器的網絡連接: Sentinel 將成爲主服務器的客戶端, 它可以向主服務器發送命令, 並從命令回覆中獲取相關的信息。
  對於每個被 Sentinel 監視的主服務器來說, Sentinel 會創建兩個連向主服務器的異步網絡連接:

  • 一個是命令連接, 這個連接專門用於向主服務器發送命令, 並接收命令回覆。
  • 一個是訂閱連接, 這個連接專門用於訂閱主服務器的 __sentinel__:hello頻道。

  需要兩個鏈接的原因:
  在 Redis 目前的發佈與訂閱功能中, 被髮送的信息都不會保存在 Redis 服務器裏面, 如果在信息發送時, 想要接收信息的客戶端不在線或者斷線, 那麼這個客戶端就會丟失這條信息。
  因此, 爲了不丟失 __sentinel__:hello頻道的任何信息, Sentinel 必須專門用一個訂閱連接來接收該頻道的信息。
  而另一方面, 除了訂閱頻道之外, Sentinel 還又必須向主服務器發送命令, 以此來與主服務器進行通訊, 所以 Sentinel 還必須向主服務器創建命令連接。並且因爲 Sentinel 需要與多個實例創建多個網絡連接, 所以 Sentinel 使用的是異步連接。

3.3 集羣

  一個 Redis 集羣通常由多個節點(node)組成, 在剛開始的時候, 每個節點都是相互獨立的, 它們都處於一個只包含自己的集羣當中, 要組建一個真正可工作的集羣, 我們必須將各個獨立的節點連接起來, 構成一個包含多個節點的集羣。

3.3.1 鏈接節點

  向一個節點 node 發送CLUSTER MEET命令, 可以讓 node 節點與 ip 和 port 所指定的節點進行握手(handshake), 當握手成功時, node節點就會將 ip 和 port 所指定的節點添加到 node 節點當前所在的集羣中。

CLUSTER MEET <ip> <port>

3.3.2 啓動節點

  一個節點就是一個運行在集羣模式下的 Redis 服務器, Redis 服務器在啓動時會根據 cluster-enabled配置選項的是否爲 yes 來決定是否開啓服務器的集羣模式。
  節點(運行在集羣模式下的 Redis 服務器)會繼續使用所有在單機模式中使用的服務器組件, 比如說:

  • 節點會繼續使用文件事件處理器來處理命令請求和返回命令回覆;
  • 節點會繼續使用時間事件處理器來執行 serverCron函數, 而 serverCron函數又會調用集羣模式特有的 clusterCron函數: clusterCron函數負責執行在集羣模式下需要執行的常規操作, 比如向集羣中的其他節點發送 Gossip 消息, 檢查節點是否斷線; 又或者檢查是否需要對下線節點進行自動故障轉移, 等等;
  • 節點會繼續使用數據庫來保存鍵值對數據,鍵值對依然會是各種不同類型的對象;
  • 節點會繼續使用 RDB 持久化模塊和 AOF 持久化模塊來執行持久化工作;
  • 節點會繼續使用發佈與訂閱模塊來執行 PUBLISH 、 SUBSCRIBE等命令;
  • 節點會繼續使用複製模塊來進行節點的複製工作;
  • 節點會繼續使用 Lua 腳本環境來執行客戶端輸入的 Lua 腳本。

  除此之外, 節點會繼續使用redisServer結構來保存服務器的狀態, 使用 redisClient結構來保存客戶端的狀態, 至於那些只有在集羣模式下才會用到的數據, 節點將它們保存到了 cluster.h/clusterNode結構,cluster.h/clusterLink結構, 以及 cluster.h/clusterState結構裏面。

3.3.3 集羣數據結構

  clusterNode結構保存了一個節點的當前狀態。每個節點都會使用一個 clusterNode結構來記錄自己的狀態, 併爲集羣中的所有其他節點(包括主節點和從節點)都創建一個相應的clusterNode結構, 以此來記錄其他節點的狀態。

typedef struct clusterNode {
    mstime_t ctime; /* Node object creation time. */
    char name[CLUSTER_NAMELEN]; /* Node name, hex string, sha1-size */
    int flags;      /* CLUSTER_NODE_... */
    uint64_t configEpoch; /* Last configEpoch observed for this node */
    unsigned char slots[CLUSTER_SLOTS/8]; /* slots handled by this node */
    int numslots;   /* Number of slots handled by this node */
    int numslaves;  /* Number of slave nodes, if this is a master */
    struct clusterNode **slaves; /* pointers to slave nodes */
    struct clusterNode *slaveof; /* pointer to the master node. Note that it
                                    may be NULL even if the node is a slave
                                    if we don't have the master node in our
                                    tables. */
    mstime_t ping_sent;      /* Unix time we sent latest ping */
    mstime_t pong_received;  /* Unix time we received the pong */
    mstime_t fail_time;      /* Unix time when FAIL flag was set */
    mstime_t voted_time;     /* Last time we voted for a slave of this master */
    mstime_t repl_offset_time;  /* Unix time we received offset for this node */
    mstime_t orphaned_time;     /* Starting time of orphaned master condition */
    long long repl_offset;      /* Last known repl offset for this node. */
    char ip[NET_IP_STR_LEN];  /* Latest known IP address of this node */
    int port;                   /* Latest known clients port of this node */
    int cport;                  /* Latest known cluster port of this node. */
    clusterLink *link;          /* TCP/IP link with this node */
    list *fail_reports;         /* List of nodes signaling this as failing */
} clusterNode;

  clusterNode結構的 link屬性是一個clusterLink結構, 該結構保存了連接節點所需的有關信息, 比如套接字描述符, 輸入緩衝區和輸出緩衝區。

/* clusterLink encapsulates everything needed to talk with a remote node. */
typedef struct clusterLink {
    mstime_t ctime;             /* Link creation time */
    int fd;                     /* TCP socket file descriptor */
    sds sndbuf;                 /* Packet send buffer */
    sds rcvbuf;                 /* Packet reception buffer */
    struct clusterNode *node;   /* Node related to this link if any, or NULL */
} clusterLink;

  redisClient 結構和 clusterLink 結構都有自己的套接字描述符和輸入、輸出緩衝區, 這兩個結構的區別在於, redisClient 結構中的套接字和緩衝區是用於連接客戶端的, 而 clusterLink 結構中的套接字和緩衝區則是用於連接節點的。
  每個節點都保存着一個 clusterState 結構, 這個結構記錄了在當前節點的視角下, 集羣目前所處的狀態 —— 比如集羣是在線還是下線, 集羣包含多少個節點, 集羣當前的配置紀元, 諸如此類。

typedef struct clusterState {
    clusterNode *myself;  /* This node */
    uint64_t currentEpoch;
    int state;            /* CLUSTER_OK, CLUSTER_FAIL, ... */
    int size;             /* Num of master nodes with at least one slot */
    dict *nodes;          /* Hash table of name -> clusterNode structures */
    dict *nodes_black_list; /* Nodes we don't re-add for a few seconds. */
    clusterNode *migrating_slots_to[CLUSTER_SLOTS];
    clusterNode *importing_slots_from[CLUSTER_SLOTS];
    clusterNode *slots[CLUSTER_SLOTS];
    uint64_t slots_keys_count[CLUSTER_SLOTS];
    rax *slots_to_keys;
    /* The following fields are used to take the slave state on elections. */
    mstime_t failover_auth_time; /* Time of previous or next election. */
    int failover_auth_count;    /* Number of votes received so far. */
    int failover_auth_sent;     /* True if we already asked for votes. */
    int failover_auth_rank;     /* This slave rank for current auth request. */
    uint64_t failover_auth_epoch; /* Epoch of the current election. */
    int cant_failover_reason;   /* Why a slave is currently not able to
                                   failover. See the CANT_FAILOVER_* macros. */
    /* Manual failover state in common. */
    mstime_t mf_end;            /* Manual failover time limit (ms unixtime).
                                   It is zero if there is no MF in progress. */
    /* Manual failover state of master. */
    clusterNode *mf_slave;      /* Slave performing the manual failover. */
    /* Manual failover state of slave. */
    long long mf_master_offset; /* Master offset the slave needs to start MF
                                   or zero if stil not received. */
    int mf_can_start;           /* If non-zero signal that the manual failover
                                   can start requesting masters vote. */
    /* The followign fields are used by masters to take state on elections. */
    uint64_t lastVoteEpoch;     /* Epoch of the last vote granted. */
    int todo_before_sleep; /* Things to do in clusterBeforeSleep(). */
    /* Messages received and sent by type. */
    long long stats_bus_messages_sent[CLUSTERMSG_TYPE_COUNT];
    long long stats_bus_messages_received[CLUSTERMSG_TYPE_COUNT];
    long long stats_pfail_nodes;    /* Number of nodes in PFAIL status,
                                       excluding nodes without address. */
} clusterState;

3.3.4 CLUSTER MEET 命令的實現

  1. 節點 A 會爲節點 B 創建一個 clusterNode結構, 並將該結構添加到自己的 clusterState.nodes字典裏面。
  2. 之後, 節點 A 將根據 CLUSTER MEET命令給定的 IP 地址和端口號, 向節點 B 發送一條 MEET 消息(message)。
  3. 如果一切順利, 節點 B 將接收到節點 A 發送的 MEET 消息, 節點 B 會爲節點 A 創建一個 clusterNode結構, 並將該結構添加到自己的 clusterState.nodes字典裏面。
  4. 之後, 節點 B 將向節點 A 返回一條 PONG 消息。
  5. 如果一切順利, 節點 A 將接收到節點 B 返回的 PONG消息, 通過這條 PONG 消息節點 A 可以知道節點 B 已經成功地接收到了自己發送的 MEET 消息。
  6. 之後, 節點 A 將向節點 B 返回一條PING消息。
  7. 如果一切順利, 節點 B 將接收到節點 A 返回的 PING 消息, 通過這條PING消息節點 B 可以知道節點 A 已經成功地接收到了自己返回的 PONG消息, 握手完成。

  之後, 節點 A 會將節點 B 的信息通過 Gossip協議傳播給集羣中的其他節點, 讓其他節點也與節點 B 進行握手, 最終, 經過一段時間之後, 節點 B 會被集羣中的所有節點認識。

4 獨立功能的實現

4.1 頻道訂閱與退訂

  當一個客戶端執行 SUBSCRIBE命令, 訂閱某個或某些頻道的時候, 這個客戶端與被訂閱頻道之間就建立起了一種訂閱關係。redis將所有頻道的訂閱關係都保存在服務器狀態的 pubsub_channels字典裏面, 這個字典的鍵是某個被訂閱的頻道, 而鍵的值則是一個鏈表, 鏈表裏面記錄了所有訂閱這個頻道的客戶端。

struct redisServer{
    dict *pubsub_channels;  /* channels a client is interested in (SUBSCRIBE) */
    list *pubsub_patterns;  /* patterns a client is interested in (SUBSCRIBE) */
};

4.1.1 訂閱頻道

  每當客戶端執行 SUBSCRIBE命令, 訂閱某個或某些頻道的時候, 服務器都會將客戶端與被訂閱的頻道在 pubsub_channels字典中進行關聯。

  根據頻道是否已經有其他訂閱者, 關聯操作分爲兩種情況執行:

  • 如果頻道已經有其他訂閱者, 那麼它在 pubsub_channels字典中必然有相應的訂閱者鏈表, 程序唯一要做的就是將客戶端添加到訂閱者鏈表的末尾;
  • 如果頻道還未有任何訂閱者, 那麼它必然不存在於 pubsub_channels字典, 程序首先要在 pubsub_channels字典中爲頻道創建一個鍵, 並將這個鍵的值設置爲空鏈表, 然後再將客戶端添加到鏈表, 成爲鏈表的第一個元素。

4.1.2 退訂頻道

  UNSUBSCRIBE命令的行爲和 SUBSCRIBE命令的行爲正好相反 —— 當一個客戶端退訂某個或某些頻道的時候, 服務器將從pubsub_channels中解除客戶端與被退訂頻道之間的關聯:

  • 程序會根據被退訂頻道的名字, 在 pubsub_channels字典中找到頻道對應的訂閱者鏈表, 然後從訂閱者鏈表中刪除退訂客戶端的信息;
  • 如果刪除退訂客戶端之後, 頻道的訂閱者鏈表變成了空鏈表, 那麼說明這個頻道已經沒有任何訂閱者了, 程序將從 pubsub_channels字典中刪除頻道對應的鍵。

4.2 事務

  一個事務從開始到結束通常會經歷以下三個階段:

  1. 事務開始;
  2. 命令入隊;
  3. 事務執行。

  事務開始:
  MULTI命令的執行標誌着事務的開始。MULTI命令可以將執行該命令的客戶端從非事務狀態切換至事務狀態, 這一切換是通過在客戶端狀態的 flags屬性中打開 CLIENT_MULTI標識來完成的。

  事務入隊:
  當一個客戶端處於非事務狀態時, 這個客戶端發送的命令會立即被服務器執行。與此不同的是, 當一個客戶端切換到事務狀態之後, 服務器會根據這個客戶端發來的不同命令執行不同的操作:

  如果客戶端發送的命令爲 EXEC 、 DISCARD 、 WATCH 、 MULTI四個命令的其中一個, 那麼服務器立即執行這個命令。
  與此相反, 如果客戶端發送的命令是 EXEC 、 DISCARD 、 WATCH 、 MULTI四個命令以外的其他命令, 那麼服務器並不立即執行這個命令, 而是將這個命令放入一個事務隊列裏面, 然後向客戶端返回 QUEUED回覆。

  事務隊列:
  每個 Redis 客戶端都有自己的事務狀態, 這個事務狀態保存在客戶端狀態的 mstate屬性裏面。事務狀態包含一個事務隊列, 以及一個已入隊命令的計數器 (也可以說是事務隊列的長度)。事務隊列是一個 multiCmd 類型的數組, 數組中的每個 multiCmd結構都保存了一個已入隊命令的相關信息, 包括指向命令實現函數的指針, 命令的參數, 以及參數的數量。事務隊列以先進先出(FIFO)的方式保存入隊的命令: 較先入隊的命令會被放到數組的前面, 而較後入隊的命令則會被放到數組的後面。

struct client{
multiState mstate;
};

typedef struct multiState {
    multiCmd *commands;     /* Array of MULTI commands */
    int count;              /* Total number of MULTI commands */
    int cmd_flags;          /* The accumulated command flags OR-ed together.
                               So if at least a command has a given flag, it
                               will be set in this field. */
    int minreplicas;        /* MINREPLICAS for synchronous replication */
    time_t minreplicas_timeout; /* MINREPLICAS timeout as unixtime. */
} multiState;

/* Client MULTI/EXEC state */
typedef struct multiCmd {
    robj **argv;
    int argc;
    struct redisCommand *cmd;
} multiCmd;

  執行事務:
  當一個處於事務狀態的客戶端向服務器發送 EXEC命令時, 這個 EXEC命令將立即被服務器執行: 服務器會遍歷這個客戶端的事務隊列, 執行隊列中保存的所有命令, 最後將執行命令所得的結果全部返回給客戶端。

4.3 lua腳本

4.4 排序

  SORT命令可以對數據進行排序。SORT 命令爲每個被排序的鍵都創建一個與鍵長度相同的數組, 數組的每個項都是一個 redisSortObject 結構, 根據 SORT 命令使用的選項不同, 程序使用 redisSortObject 結構的方式也不同。

typedef struct _redisSortObject {
    robj *obj;
    union {
        double score;
        robj *cmpobj;
    } u;
} redisSortObject;

typedef struct _redisSortOperation {
    int type;
    robj *pattern;
} redisSortOperation;

4.5 二進制位數組

  GETBIT 命令用於返回位數組 bitarray 在 offset 偏移量上的二進制位的值。
  GETBIT 命令的執行過程如下:

  1. 計算 $byte = \lfloor offset \div 8 \rfloor $, byte 值記錄了 offset 偏移量指定的二進制位保存在位數組的哪個字節。
  2. 計算 $bit = (offset \bmod 8) + 1 $, bit 值記錄了 offset 偏移量指定的二進制位是 byte 字節的第幾個二進制位。
  3. 根據 byte 值和 bit 值, 在位數組 bitarray 中定位 offset 偏移量指定的二進制位, 並返回這個位的值。

4.6 慢查詢日誌

  Redis 的慢查詢日誌功能用於記錄執行時間超過給定時長的命令請求, 用戶可以通過這個功能產生的日誌來監視和優化查詢速度。

4.7 監視器

  發送 MONITOR 命令可以讓一個普通客戶端變爲一個監視器。

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章