關鍵字:空間預分配,惰性空間釋放,2進制安全
C字符串不容易更改,所以Redis中把C字符串用在1些不必對字符串值進行修改的地方,作為字符串字面量(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[];
};
獲得1個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都是2進制安全的(binary-safe),所有SDS API都會以2進制的方式處理SDS寄存在buf數組里的數據,程序不會對其中的數據做任何限制、過濾、或假定,數據在寫入時是甚么樣的,被讀取時就是甚么樣。
Redis用SDS的buf數組保存2進制數據而不是字符。
SDS可以兼容部份C字符串函數。
關鍵字:多態
當1個列表鍵包括了數量比較多的元素,或是列表中包括的元素都是比較長的字符串時,Redis就會使用鏈表作為列表鍵的底層實現。
integers列表鍵的底層實現就是1個鏈表,鏈表中的每一個結點都保存了1個整數值。
除鏈表以外,發布與定閱、慢查詢、監視器等功能也用到了鏈表,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成員則是用于實現多態鏈表所需的類型特定函數:
Redis的鏈表實現的特性以下:
關鍵字:多態,漸進式rehash,murmurhash2
Redis的數據庫就是使用字典來作為底層實現的,對數據庫的增、刪、改、查也是構建在對字典的操作之上的。
字典還是哈希鍵的底層實現之1,當1個哈希鍵包括的鍵值對照較多,或是鍵值對中的元素都是比較長的字符串時,Redis就使用字典作為哈希鍵的底層實現。
Redis的字典使用哈希表作為底層實現,1個哈希表里可以有多個哈希表結點,每一個哈希表結點就保存了字典中的1個鍵值對。
字典所使用的哈希表在dict.h中定義:
/*
* 哈希表
* 每一個字典都使用兩個哈希表,從而實現漸進式 rehash 。
*/
typedef struct dictht {
// 哈希表數組,數組中的每一個元素都是1個指向dictEntry結構的指針
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩碼,用于計算索引值
// 總是等于 size - 1
unsigned long sizemask;
// 該哈希表已有節點的數量
unsigned long used;
} dictht;
/*
* 哈希表節點
*/
typedef struct dictEntry {
// 鍵
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下個哈希表節點,構成鏈表
struct dictEntry *next;
} dictEntry;
/*
* 字典
*/
typedef struct dict {
// 類型特定函數
dictType *type;
// 私有數據
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引
// 當 rehash 不在進行時,值為 ⑴
int rehashidx; /* rehashing not in progress if rehashidx == ⑴ */
// 目前正在運行的安全迭代器的數量
int iterators; /* number of iterators currently running */
} dict;
type屬性和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;
/*
* 字典迭代器
*
- 如果 safe 屬性的值為 1 ,那末在迭代進行的進程中,
- 程序依然可以履行 dictAdd 、 dictFind 和其他函數,對字典進行修改。
*
- 如果 safe 不為 1 ,那末程序只會調用 dictNext 對字典進行迭代,
- 而不對字典進行修改。
*/
typedef struct dictIterator {
// 被迭代的字典
dict *d;
// table :正在被迭代的哈希表號碼,值可以是 0 或 1 。
// index :迭代器當前所指向的哈希表索引位置。
// safe :標識這個迭代器是不是安全
int table, index, safe;
// entry :當前迭代到的節點的指針
// nextEntry :當前迭代節點的下1個節點
// 由于在安全迭代器運作時, entry 所指向的節點可能會被修改,
// 所以需要1個額外的指針來保存下1節點的位置,
// 從而避免指針丟失
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)保持在1個公道的范圍以內,當哈希表保存的鍵值對數量太多或太少時,程序需要對哈希表的大小進行相應的擴大或收縮。
擴大和收縮哈希表的工作可以通過履行rehash(重新散列)操作來完成,Redis對字典的哈希表履行rehash的步驟以下:
為字典的ht[1]哈希表分配空間,這個哈希表的空間大小取決于要履行的操作,和ht[0]當前包括的鍵值對數量(即ht[0].used屬性的值)
將保存在ht[0]中的所有鍵值對rehash到ht[1]上面:rehash指的是重新計算鍵的哈希值和索引值,然后將鍵值對放置到ht[1]哈希表的指定位置上。
當以下條件中的任意1個被滿足時,程序會自動開始對哈希表履行擴大操作:
在履行BGSAVE命令或BGREWRITEAOF命令的進程中,Redis需要創建當前服務器進程的子進程,而大多數操作系統都采取寫時復制(copy-on-write)技術來優化子進程的使用效力,所以在子進程存在期間,服務器會提高履行擴大操作所需的負載因子,從而盡量地避免在子進程存在期間進行哈希表擴大操作,這避免了沒必要要的內存寫入操作,最大限度地節儉內存。
當哈希表的負載因子小于0.1時,程序自動開始對哈希表履行收縮操作。
為了不rehash對服務器性能造成影響,服務器不是1次性將ht[0]里面的所有鍵值對全部rehash到ht[1],而是分屢次、漸進式地將ht[0]里面的鍵值對漸漸rehash到ht[1]。
以下是哈希表漸進式rehash的詳細步驟:
為ht[1]分配空間,讓字典同時持有ht[0]和ht[1]兩個哈希表。
在字典中保持1個索引計數器變量rehashidx,值設置為0,表示rehash工作正式開始
在rehash進行期間,每次對字典履行添加、刪除、查找或更新操作時,程序除履行指定的操作以為,還會順帶將ht[0]哈希表在rehashidx索引上的所有鍵值對rehash到ht[1],當rehash工作完成以后,程序將rehashidx屬性的值增1。
隨著字典操作的不斷履行,終究在某個時間點上,ht[0]的所有鍵值對都會被rehash到ht[1]上,這是程序將rehashidx屬性的值設為⑴,表示rehash操作已完成
漸進式rehash采取分而治之的方式,將rehash鍵值對所需的計算工作均攤到對字典的每一個添加、刪除、查找和更新操作上,從而避免了集中式rehash而帶來的龐大計算量。
在進行漸進式rehash的進程中,字典會同時使用ht[0]和ht[1]兩個哈希表,所以在漸進式rehash進行期間,字典的刪除、查找、更新會在兩個哈希表上進行,比如現在ht[0]中查找,沒找到再去ht[1]查找
在漸進式rehash履行期間,新添加到字典的鍵值對1律會被保存到ht[1]里面,而ht[0]則不再進行任何添加操作,這樣保證了ht[0]包括的鍵值對數量只減不增,隨著rehash操作的履行終究變成空表。
上一篇 Jsonp解決ajax跨域問題
下一篇 再談終端設備