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()拷貝字符串的內容。