標籤:
一、簡單動態字串SDS
關鍵字:空間預分配,惰性空間釋放,二進位安全
C字串不易更改,所以Redis中把C字串用在一些無須對字串值進行修改的地方,作為字串字面量(String literal),比如列印日誌:
redisLog(REDIS_WARING, “Redis is now ready to exit, bye bye…”);
在Redis資料庫中,包含字串的索引值對在底層都是由SDS實現的。
SDS還被用作緩衝區(buffer):AOF模組中的AOF緩衝區,以及用戶端狀態中的輸入緩衝區,都是SDS實現的。
源碼
SDS結構的定義在sds.h中:
/* * 儲存字串對象的結構 */ struct sdshdr { // buf 中已佔用空間的長度 int len; // buf 中剩餘可用空間的長度,即未使用空間 int free; // 資料空間 char buf[]; };
擷取一個SDS長度的複雜度為O(1),由SDS的API在執行時自動化佈建和更新SDS長度,使用SDS無須進行任何手動修改長度的工作。
空間分配
SDS的空間分配策略是:當SDS API需要對SDS進行修改時,API會先檢查SDS的空間是否滿足修改所需的要求,若不滿足,API會自動將SDS的空間擴充至執行修改所需的大小,然後才執行實際的修改操作,杜絕了發生緩衝區溢位的可能性。
通過未使用空間,SDS實現了空間預分配和惰性空間釋放兩種最佳化策略:
空間預分配用於減少連續執行字串增長操作所需的記憶體配置次數。
通過這種預分配策略,SDS將連續增長N次字串所需的記憶體重分配次數從必定N次降低為最多N次。
其中額外分配的未使用空間數量由以下公式決定:
1. 如果對SDS進行修改後,SDS的長度(即len屬性的值)小於1MB,就分配和len屬性同樣大小的未使用空間,即len屬性的值和free屬性的值相同 2. 如果對SDS進行修改之後,SDS的長度大於等於1MB,就分配1MB的未使用空間。
惰性空間釋放用於最佳化SDS字串縮短操作的記憶體重分配操作:當SDS的API需要縮短SDS儲存的字串時,程式並不立即使用記憶體重分配來回收縮短後多出來的位元組,而是使用free屬性將這些位元組的數量記錄起來,並等待將來使用。
SDS的API都是二進位安全的(binary-safe),所有SDS API都會以二進位的方式處理SDS存放在buf數組裡的資料,程式不會對其中的資料做任何限制、過濾、或者假設,資料在寫入時是什麼樣的,被讀取時就是什麼樣。
Redis用SDS的buf數組儲存位元據而不是字元。
SDS可以相容部分C字串函數。
二、鏈表
關鍵字:多態
當一個列表鍵包含了數量比較多的元素,或是列表中包含的元素都是比較長的字串時,Redis就會使用鏈表作為列表鍵的底層實現。
integers列表鍵的底層實現就是一個鏈表,鏈表中的每個結點都儲存了一個整數值。
除了鏈表之外,發布與訂閱、慢查詢、監視器等功能也用到了鏈表,Redis伺服器本身還使用鏈表儲存多個用戶端的狀態資訊,以及使用鏈表來構建用戶端輸出緩衝區(output buffer)。
源碼
鏈表結構的定義在adlist.h中:
/* - 雙端鏈表節點 */ typedef struct listNode { // 前置節點 struct listNode *prev; // 後置節點 struct listNode *next; // 節點的值 void *value; } listNode; /* *雙端鏈表迭代器 */ typedef struct listIter { // 當前迭代到的節點 listNode *next; // 迭代的方向 int direction; } listIter; /* - 雙端鏈表結構 */ typedef struct list { // 表前端節點 listNode *head; // 表尾節點 listNode *tail; // 節點值複製函數 void *(*dup)(void *ptr); // 節點值釋放函數 void (*free)(void *ptr); // 節點值對比函數 int (*match)(void *ptr, void *key); // 鏈表所包含的節點數量 unsigned long len; } list;
list結構為鏈表提供了表頭指標head、表尾指標tail,以及鏈表長度計數器len,dup、free和match成員則是用於實現多態鏈表所需的類型特定函數:
- dup函數用於複製鏈表結點所儲存的值
- free函數用於釋放鏈表結點所儲存的值;
- match函數則用於對比鏈表結點所儲存的值和另一個輸入值是否相等。
Redis的鏈表實現的特性如下:
- 雙端、無環、帶表頭指標和表尾指標、帶鏈表長度計數器、多態
三、字典
關鍵字:多態,漸進式rehash,murmurhash2
Redis的資料庫就是使用字典來作為底層實現的,對資料庫的增、刪、改、查也是構建在對字典的操作之上的。
字典還是雜湊鍵的底層實現之一,當一個雜湊鍵包含的索引值對比較多,或是索引值對中的元素都是比較長的字串時,Redis就使用字典作為雜湊鍵的底層實現。
Redis的字典使用雜湊表作為底層實現,一個雜湊表裡可以有多個雜湊表結點,每個雜湊表結點就儲存了字典中的一個索引值對。
源碼
字典所使用的雜湊表在dict.h中定義:
/* * 雜湊表 * 每個字典都使用兩個雜湊表,從而實現漸進式 rehash 。 */ typedef struct dictht { // 雜湊表數組,數組中的每個元素都是一個指向dictEntry結構的指標 dictEntry **table; // 雜湊表大小 unsigned long size; // 雜湊表大小掩碼,用於計算索引值 // 總是等於 size - 1 unsigned long sizemask; // 該雜湊表已有節點的數量 unsigned long used; } dictht;
- table屬性是一個數組,數組中的每個元素都是一個指向dictEntry結構的指標,每個dictEntry結構儲存著一個索引值對。
- size屬性記錄了雜湊表的大小,即是table數組的大小。
- used屬性則記錄了雜湊表目前已有結點(索引值對的數量)
- sizemask屬性和雜湊值一起決定一個鍵應該被放到table數組的哪個索引上面
/* * 雜湊表節點 */ typedef struct dictEntry { // 鍵 void *key; // 值 union { void *val; uint64_t u64; int64_t s64; } v; // 指向下個雜湊表節點,形成鏈表 struct dictEntry *next; } dictEntry;
- key屬性儲存著索引值對中的鍵
- v屬性儲存索引值對中的值,其中索引值對中的值可以是一個指標,或是一個uint64_t整數,或是一個int64_t整數
- next屬性指向另一個雜湊表結點的指標,使用鏈地址法解決鍵衝突問題。
/* * 字典 */ typedef struct dict { // 類型特定函數 dictType *type; // 私人資料 void *privdata; // 雜湊表 dictht ht[2]; // rehash 索引 // 當 rehash 不在進行時,值為 -1 int rehashidx; /* rehashing not in progress if rehashidx == -1 */ // 目前正在啟動並執行安全迭代器的數量 int iterators; /* number of iterators currently running */ } dict;
type屬性和privdata屬性是針對不同類型的索引值對,為建立多態字典而設定的:
- type屬性是一個指向dictType結構的指標,每個dictType結構儲存了一簇用於操作特定類型索引值對的函數,Redis會為用途不同的字典設定不同的類型特定函數。
- privdata屬性儲存了需要傳給那些類型特定函數的選擇性參數。
/* * 字典類型特定函數 */typedef struct dictType { // 計算雜湊值的函數 unsigned int (*hashFunction)(const void *key); // 複製鍵的函數 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;
- ht屬性是一個包含兩個項的數組,數組中的每個項都是一個dictht雜湊表,一般,字典只使用ht[0]雜湊表,ht[1]雜湊表只會在對ht[0]雜湊表進行rehash時使用。
- rehashidx屬性記錄了rehash目前的進度,如果目前沒有在進行rehash,那麼它的值為-1.
/* * 字典迭代器 * - 如果 safe 屬性的值為 1 ,那麼在迭代進行的過程中, - 程式仍然可以執行 dictAdd 、 dictFind 和其他函數,對字典進行修改。 * - 如果 safe 不為 1 ,那麼程式只會調用 dictNext 對字典進行迭代, - 而不對字典進行修改。 */ typedef struct dictIterator { // 被迭代的字典 dict *d; // table :正在被迭代的雜湊表號碼,值可以是 0 或 1 。 // index :迭代器當前所指向的雜湊表索引位置。 // safe :標識這個迭代器是否安全 int table, index, safe; // entry :當前迭代到的節點的指標 // nextEntry :當前迭代節點的下一個節點 // 因為在安全迭代器運作時, entry 所指向的節點可能會被修改, // 所以需要一個額外的指標來儲存下一節點的位置, // 從而防止指標丟失 dictEntry *entry, *nextEntry; long long fingerprint; /* unsafe iterator fingerprint for misuse detection */ } dictIterator;
雜湊
Redis計算雜湊值和索引值的方法如下:
// 使用字典設定的雜湊函數,計算鍵key的雜湊值 hash = dict->type->hashFunction(key); // 使用雜湊表的sizemask屬性和雜湊值,計算出索引值 // 根據情況不同,ht[x]可以是ht[0]或ht[1] index = hash & dict->ht[x].sizemask;
/* ------------------------- hash functions ------------------------------ *//* Thomas Wang‘s 32 bit Mix Function */unsigned int dictIntHashFunction(unsigned int key){ key += ~(key << 15); key ^= (key >> 10); key += (key << 3); key ^= (key >> 6); key += ~(key << 11); key ^= (key >> 16); return key;}/* Identity hash function for integer keys */unsigned int dictIdentityHashFunction(unsigned int key){ return key;}static uint32_t dict_hash_function_seed = 5381;void dictSetHashFunctionSeed(uint32_t seed) { dict_hash_function_seed = seed;}uint32_t dictGetHashFunctionSeed(void) { return dict_hash_function_seed;}/* MurmurHash2, by Austin Appleby * Note - This code makes a few assumptions about how your machine behaves - * 1. We can read a 4-byte value from any address without crashing * 2. sizeof(int) == 4 * * And it has a few limitations - * * 1. It will not work incrementally. * 2. It will not produce the same results on little-endian and big-endian * machines. */unsigned int dictGenHashFunction(const void *key, int len) { /* ‘m‘ and ‘r‘ are mixing constants generated offline. They‘re not really ‘magic‘, they just happen to work well. */ uint32_t seed = dict_hash_function_seed; const uint32_t m = 0x5bd1e995; const int r = 24; /* Initialize the hash to a ‘random‘ value */ uint32_t h = seed ^ len; /* Mix 4 bytes at a time into the hash */ const unsigned char *data = (const unsigned char *)key; while(len >= 4) { uint32_t k = *(uint32_t*)data; k *= m; k ^= k >> r; k *= m; h *= m; h ^= k; data += 4; len -= 4; } /* Handle the last few bytes of the input array */ switch(len) { case 3: h ^= data[2] << 16; case 2: h ^= data[1] << 8; case 1: h ^= data[0]; h *= m; }; /* Do a few final mixes of the hash to ensure the last few * bytes are well-incorporated. */ h ^= h >> 13; h *= m; h ^= h >> 15; return (unsigned int)h;}/* And a case insensitive hash function (based on djb hash) */unsigned int dictGenCaseHashFunction(const unsigned char *buf, int len) { unsigned int hash = (unsigned int)dict_hash_function_seed; while (len--) hash = ((hash << 5) + hash) + (tolower(*buf++)); /* hash * 33 + c */ return hash;}
當字典被用作資料庫的底層實現,或是雜湊鍵的底層實現時,Redis使用MurmurHash2演算法計算鍵的雜湊值:
- 該演算法的優點在於,即使輸入的鍵是有規律的,演算法仍能給出一個很好的隨機分布性,並且演算法的計算速度也非常快。
為了讓雜湊表的負載因子(load factor)維持在一個合理的範圍之內,當雜湊表儲存的索引值對數量太多或太少時,程式需要對雜湊表的大小進行相應的擴充或收縮。
- 雜湊表的負載因子計算公式:load_factor = ht[0].used/ht[0].size
rehash
擴充和收縮雜湊表的工作可以通過執行rehash(重新散列)操作來完成,Redis對字典的雜湊表執行rehash的步驟如下:
當以下條件中的任意一個被滿足時,程式會自動開始對雜湊表執行擴充操作:
- 伺服器目前沒有在執行BGSAVE命令或BGREWRITEAOF命令,並且雜湊表的負載因子大於等於1
- 伺服器目前正在執行BGSAVE命令或BGREWRITEAOF命令,並且雜湊表的負載因子大於等於5
在執行BGSAVE命令或BGREWRITEAOF命令的過程中,Redis需要建立當前伺服器處理序的子進程,而大多數作業系統都採用寫時複製(copy-on-write)技術來最佳化子進程的使用效率,所以在子進程存在期間,伺服器會提高執行擴充操作所需的負載因子,從而儘可能地避免在子進程存在期間進行雜湊表擴充操作,這避免了不必要的記憶體寫入操作,最大限度地節約記憶體。
當雜湊表的負載因子小於0.1時,程式自動開始對雜湊表執行收縮操作。
漸進式rehash
為了避免rehash對伺服器效能造成影響,伺服器不是一次性將ht[0]裡面的所有索引值對全部rehash到ht[1],而是分多次、漸進式地將ht[0]裡面的索引值對慢慢rehash到ht[1]。
以下是雜湊表漸進式rehash的詳細步驟:
為ht[1]分配空間,讓字典同時持有ht[0]和ht[1]兩個雜湊表。
在字典中維持一個索引計數器變數rehashidx,值設定為0,表示rehash工作正式開始
在rehash進行期間,每次對字典執行添加、刪除、尋找或者更新操作時,程式除了執行指定的操作以為,還會順帶將ht[0]雜湊表在rehashidx索引上的所有索引值對rehash到ht[1],當rehash工作完成之後,程式將rehashidx屬性的值增一。
隨著字典操作的不斷執行,最終在某個時間點上,ht[0]的所有索引值對都會被rehash到ht[1]上,這是程式將rehashidx屬性的值設為-1,表示rehash操作已完成
漸進式rehash採取分而治之的方式,將rehash索引值對所需的計算工作均攤到對字典的每個添加、刪除、尋找和更新操作上,從而避免了集中式rehash而帶來的龐大計算量。
在進行漸進式rehash的過程中,字典會同時使用ht[0]和ht[1]兩個雜湊表,所以在漸進式rehash進行期間,字典的刪除、尋找、更新會在兩個雜湊表上進行,比如現在ht[0]中尋找,沒找到再去ht[1]尋找
在漸進式rehash執行期間,新添加到字典的索引值對一律會被儲存到ht[1]裡面,而ht[0]則不再進行任何添加操作,這樣保證了ht[0]包含的索引值對數量只減不增,隨著rehash操作的執行最終變成空表。
《Redis設計與實現》[第一部分]資料結構與對象-C源碼閱讀(一)