Redis源碼閱讀 (基本數據結構)

1. Redis介紹

Redis是一個開源的Key-Value存儲引擎。它支持string、hash、 list、 set和sorted set等多種值類型。

2. 基本功能

2.1. 鏈表(adlist.h/adlist.c)

鏈表(list)是Redis中最基本的數據結構,由adlist.h和adlist.c定義。

listNode是最基本的結構,表示鏈表中的一個結點。結點有向前(next)和向後(prev)連個指針,鏈表是雙向鏈表。保存的值是void*類型。

鏈表通過list定義,提供頭(head)、尾(tail)兩個指針,分別指向頭部的結點和尾部的結點;提供三個函數指針,供用戶傳入自定義函數,用於複製(dup)、釋放(free)和匹配(match) 鏈表中的結點的值(value);通過無符號整數len,標示鏈表的長度。

listIter 是訪問鏈表的迭代器,指針(next)指向鏈表的某個結點

direction標示迭代訪問的方向(宏AL_START_HEAD表示向前, AL_START_TAIL表示向後)。

使用方法

Redis定義了一系列的宏,用於訪問list及其內部結點。鏈表創建時(listCreate),通過Redis自己實現的zmalloc()分配堆空間。鏈表釋放(listRelease)或刪除結點(listDelNode),如果定義了鏈表(list)的指針函數free, Redis會使用它釋放鏈表的每一個結點的值(value),否則需要用戶手動釋放。結點的內存使用Redis自己實現的zfree()釋放。對於迭代器,通過方法listGetIterator()、 listNext()、listReleaseIterator()、 listRewind()和listRewindTail()使用,例如對於鏈表list,要從頭向尾遍歷

可通過如下代碼:

iter = listGetIterator(list, AL_START_HEAD); // 獲取迭代器
while((node = listNext(iter)) != NULL) {
      DoItWithValue(listNodeValue(node)); // 用戶實現DoItWithValue
}
listReleaseIterator(iter);

listDup()用於複製鏈表,如果用戶實現了dup函數,則會使用它複製鏈表結點的value。 listSearchKey()通過循環的方式在O(N)的時間複雜度下查找值,若用戶實現了match函數,則用它進行匹配,否則使用按引用匹配。

2.2. 字符串

https://blog.csdn.net/alpha_love/article/details/106581437

2.3. 哈希表(dict.h/dict.c)

Redis的哈希表最大的特色就是自動擴容。當它的哈希表容量不夠時,可以0/1切換,然後自動擴容。下面具體分析哈希表的實現。整個哈希系統由結構體dict定義,其中type包含一系列哈希表需要用的函數,

dictht類型的數組ht[2]表示兩個哈希表實例,由rehashidx指明下一個需要擴容的哈希實例的編號, iterators記錄外部使用哈希表的迭代器的數目。

typedef struct dict {

dictType *type;

void *privdata;

dictht ht[2];

int rehashidx; /* rehashing not in progress if rehashidx == -1 */

int iterators; /* number of iterators currently running */

} dict;

dictht爲哈希表具體實現的結構體, table指向哈希中的記錄,用數組+開鏈的形式保存記錄; size表示哈希表桶的大小,爲2的指數; sizemark= size-1,方便哈希值根據size取模; used記錄了哈希表中的記錄數目。

typedef struct dictht {

dictEntry **table;

unsigned long size;

unsigned long sizemask;

unsigned long used;

} dictht;

哈希表使用開鏈的方式處理衝突,每條記錄都是鏈表中的一個結點。 dictType在哈希系統中包含了一系列可由應用程序定義的函數指針,包括Hash函數、 Key複製、 Value複製、 Key比較、 Key析構、 Value析構,以增加哈希系統的靈活性。 其中系統定義了三種默認的type,表示最常用的三種哈希表。

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;

extern dictType dictTypeHeapStringCopyKey;

extern dictType dictTypeHeapStrings;

extern dictType dictTypeHeapStringCopyKeyValue;

Redis定義了一系列的宏用於操作哈希表,例如設置記錄的Value。對外提供的API,除了常規的創建哈希表,增、刪、改記錄之外,有兩類是比較特別的:自動擴容和迭

代器。自動擴容

Redis用變量dict_can_resize記錄哈希是否可以自動擴容,由兩個方法dictEnableResize()和dictDisableResize()設置該變量。應用程序可以使用dictResize()擴容,它首先判斷是否允許擴容,及是否正在擴容。若可以擴容,則調用dictExpand()擴容。然後應用程序需要調用dictRehashMilliseconds()啓動擴容過程,並指定擴容過程中記錄拷貝速度。除了應用程序指定的擴容外,在調用dictAdd()往哈希中添加記錄時,系統也會通過調用_dictExpandIfNeeded()判斷是否需要擴容。 _dictExpandIfNeeded()中,如果正在擴容,則不會重複進行擴容;如果哈希表size= 0,即桶數目爲0,則擴容到初始大小;否則如果used>=size,並且can_resize==1或used/size超過閥值(默認爲5)時,以max(used, size)的兩倍爲基數,調用dictExpand()擴容。

if (dictIsRehashing(d)) return DICT_OK;

if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);

if (d->ht[0].used >= d->ht[0].size &&

(dict_can_resize ||

d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))

{

return dictExpand(d, ((d->ht[0].size > d->ht[0].used) ?

d->ht[0].size : d->ht[0].used)*2);

}

return DICT_OK;

dictExpand()進行擴容時,會先選擇一個滿足size需求的2的指數,然後分配內存空間,創建新的哈希表。如果此時ht[0]爲空,則直接將哈希表賦值給ht[0];否則,賦值給ht[1],並啓動拷貝過程,將ht[0]的記錄逐個桶地拷貝到ht[1]中。置rehashidx=0,表明正在擴容,且待拷貝的桶爲ht[0]->table[rehashidx]。

dictht n; /* the new hashtable */

unsigned long realsize = _dictNextPower(size);

if (dictIsRehashing(d) || d->ht[0].used > size)

return DICT_ERR;

n.size = realsize;

n.sizemask = realsize-1;

n.table = zcalloc(realsize*sizeof(dictEntry*));

n.used = 0;

if (d->ht[0].table == NULL) {

d->ht[0] = n;

return DICT_OK;

}

d->ht[1] = n;

d->rehashidx = 0;

return DICT_OK;

dictIsRehashing()通過rehashidx來判斷是否正在擴容。這個方法在多處被調用,當dictAdd()往哈希表中添加記錄時,也會通過該方法判斷是否正在擴容。若正在擴容,則調用_dictRehashStep(),該函數判斷,若此時iterators==0,即沒有迭代器時,就從ht[0]中拷貝一部分記錄到ht[1]。拷貝過程在dictRehash()中完成,該函數返回0時,表示擴容結束, ht[0]中所有記錄都已拷貝到ht[1],且rehashidx被置爲-1;否則返回1,表示擴容未結束。拷貝過程中,將ht[0]->table[rehashidx]拷貝到ht[1]後, rehashidx++,直到used==0,即所有記錄拷貝完成。拷貝一個桶時,需要對桶中所有元素重新求哈希值,然後一個個放入ht[1]中。 dictRehash()通過參數,控制每次拷貝的桶的數目。該過程由下面代碼描述:

int dictRehash(dict *d, int n) {

if (!dictIsRehashing(d)) return 0;

while(n--) {

dictEntry *de, *nextde;

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;

}

while(d->ht[0].table[d->rehashidx] == NULL) d->rehashidx++;

de = d->ht[0].table[d->rehashidx];

while(de) {

unsigned int 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++;

}

return 1;

}

迭代器

迭代器提供了遍歷哈希表中所有元素的方法,通過dictGetIterator()獲得迭代器後,使用dictNext(dictIterator *)方法獲得下一個元素。當外部持有的迭代器數目不爲0時,哈希表會暫停擴容操作。迭代器遍歷的過程,從ht[0]開始,依次從第一個桶table[0]開始遍歷桶中的元素,然後時table[1], table[2], ..., table[size],若正在擴容,則會繼續遍歷ht[1]中的桶。遍歷桶中元素時,依次訪問鏈表中的每個元素。

2.4. 內存(zmalloc.h/zmalloc.h)

前文已提到, Redis通過自己的方法管理內存,,主要方法有zmalloc(),zrealloc(), zcalloc()和zfree(), 分別對應C中的malloc(), realloc()、calloc()和free()。相關代碼在zmalloc.h和zmalloc.c中。

Redis自己管理內存的好處主要有兩個:可以利用內存池等手段提高內存分配的性能;可以掌握更多的內存信息,以便於Redis虛擬內存(VM)等功能中,決定何時將數據swap到磁盤。

先回憶各個系統中常見的內存分配函數:

malloc()分配一塊指定大小的內存區域,並返回指向區域開頭的指針,若分配失敗,則返回NULL。

calloc()與malloc()一樣,分配一塊指定大小的內存區域,成功時返回區域頭指針,失敗返回NULL。區別在於, calloc()的輸入參數爲count和size,即分配的項的數目,及每一項的大小。 calloc()在成功分配內存空間後,會將空間內所有值置0。

realloc()修改已分配的內存塊的大小。若已分配的內存塊後沒有足夠的空間用於擴展內存塊,則重新申請一塊滿足需要的內存塊,並將舊的數據拷貝到新位置,釋放舊的內存塊,返回指向新的內存塊的指針;否則直接擴展原有的內存塊。若分配失敗,返回NULL。

free()釋放已分配的內存塊。

內存分配在Redis中,如果系統中包含TCMALLOC,則會使用tc_malloc()等TCMALLOC中的方法代替malloc()等原有的分配內存方法。 TCmalloc是google perftools中的一個組件。

#if defined(USE_TCMALLOC)

#define malloc(size) tc_malloc(size)

首先看zmalloc()和zfree()兩個最常用的方法。 Redis在申請內存時,除了申請需要的size外,還會多申請一塊定長(PREFIX_SIZE)的區域用於記錄所申請的內存塊的長度。如果申請成功, Redis會使用宏函數(Redis中爲性能考慮,大量使用宏函數)update_zmalloc_stat_alloc(size+PREFIX_SIZE, size)記錄申請的內存塊的相關信息,以便監控內存使用狀況;當內存塊被zfree()釋放時,根據頭部的信息可以快速地獲知被釋放的內存區域的長度,然後通過宏函數update_zmalloc_stat_free()標記釋放。源代碼中,若系統支持malloc_size()方法,則會使用它返回指針所指向的內存塊的大小(Mac OS X 10.4以上支持該方法[3])。 有疑惑的是,在支持malloc_size()的系統中,爲何還要多申請PREFIX_SIZE的內存?

void *zmalloc(size_t size) {

void *ptr = malloc(size+PREFIX_SIZE);

if (!ptr) zmalloc_oom(size);

#ifdef HAVE_MALLOC_SIZE

update_zmalloc_stat_alloc(redis_malloc_size(ptr),size);

return ptr;

#else

*((size_t*)ptr) = size; // 在頭部記錄內存塊的長度

update_zmalloc_stat_alloc(size+PREFIX_SIZE,size);

return (char*)ptr+PREFIX_SIZE;

#endif

}

宏update_zmalloc_stat_alloc()中,首先將要分配的空間與內存對齊,然後會根據宏zmalloc_thread_safe判斷是否需要對內存信息記錄表的相關操作加鎖。雖然Redis在大部分場景中是單線程讀寫的,即thread_safe的,但啓用虛擬內存(VM),或持久化dump到磁盤等操作時會啓動多線程,因此在多線程模式中,需要對部分操作加鎖。內存監控used_memory記錄了Redis使用的內存總數。而多線程下malloc()是線程安全的。zmalloc_allocations[]記錄了各個size分配的內存塊的數目,大於256個字節的按256算。應用程序可以通過zmalloc_allocations_for_size(size)獲得對應size的內存塊的分配數目;也可以通過zmalloc_used_memory()獲得Redis佔用的總內存。這些監控類的方法在Redis的日誌系統中被用到。

#define update_zmalloc_stat_alloc(__n,__size) do { \

size_t _n = (__n); \

size_t _stat_slot = (__size < ZMALLOC_MAX_ALLOC_STAT) ? __size :

ZMALLOC_MAX_ALLOC_STAT; \

if (_n&(sizeof(long)-1)) _n += sizeof(long)-(_n&(sizeof(long)-1)); \

if (zmalloc_thread_safe) { \

pthread_mutex_lock(&used_memory_mutex); \

used_memory += _n; \

zmalloc_allocations[_stat_slot]++; \

pthread_mutex_unlock(&used_memory_mutex); \

} else { \

used_memory += _n; \

zmalloc_allocations[_stat_slot]++; \

} \

} while(0)

外部應用程序通過zmalloc_enable_thread_safeness()方法開啓Redis內存模塊的線程安全模式,後文會分析哪些功能需要開啓線程安全。zcalloc(size)、 zrealloc()與zmalloc()的處理策略類似,不再詳述。在部分操作系統中, Redis可以通過zmalloc_get_rss()方法獲得自己的進程佔用的內存信息。該信息通過操作系統提供,往往比Redis自己記錄的used_memory更準確,但其獲取速度也較慢。這些信息也是用於虛擬內存功能。除了內存相關的操作外, Redis在此還提供了一個複製字符串的方法zstrdup(char*),該方法將申請一塊與源字符串長度相同的內存區域,並用memcpy()拷貝字符串的內容。

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