redis——数据结构(字典、链表、字符串)
1 字符串
redis并未使用傳統(tǒng)的c語(yǔ)言字符串表示,它自己構(gòu)建了一種簡(jiǎn)單的動(dòng)態(tài)字符串抽象類型。
在redis里,c語(yǔ)言字符串只會(huì)作為字符串字面量出現(xiàn),用在無(wú)需修改的地方。
當(dāng)需要一個(gè)可以被修改的字符串時(shí),redis就會(huì)使用自己實(shí)現(xiàn)的SDS(simple dynamic string)。比如在redis數(shù)據(jù)庫(kù)里,包含字符串的鍵值對(duì)底層都是SDS實(shí)現(xiàn)的,不止如此,SDS還被用作緩沖區(qū)(buffer):比如AOF模塊中的AOF緩沖區(qū)以及客戶端狀態(tài)中的輸入緩沖區(qū)。
下面來(lái)具體看一下sds的實(shí)現(xiàn):
struct sdshdr {int len;//buf已使用字節(jié)數(shù)量(保存的字符串長(zhǎng)度)int free;//未使用的字節(jié)數(shù)量char buf[];//用來(lái)保存字符串的字節(jié)數(shù)組 };sds遵循c中字符串以'\0'結(jié)尾的慣例,這一字節(jié)的空間不算在len之內(nèi)。
這樣的好處是,我們可以直接重用c中的一部分函數(shù)。比如printf;
? ? sds相對(duì)c的改進(jìn)
? ? 獲取長(zhǎng)度:c字符串并不記錄自身長(zhǎng)度,所以獲取長(zhǎng)度只能遍歷一遍字符串,redis直接讀取len即可。
? ? 緩沖區(qū)安全:c字符串容易造成緩沖區(qū)溢出,比如:程序員沒(méi)有分配足夠的空間就執(zhí)行拼接操作。而redis會(huì)先檢查sds的空間是否滿足所需要求,如果不滿足會(huì)自動(dòng)擴(kuò)充。
? ? 內(nèi)存分配:由于c不記錄字符串長(zhǎng)度,對(duì)于包含了n個(gè)字符的字符串,底層總是一個(gè)長(zhǎng)度n+1的數(shù)組,每一次長(zhǎng)度變化,總是要對(duì)這個(gè)數(shù)組進(jìn)行一次內(nèi)存重新分配的操作。因?yàn)閮?nèi)存分配涉及復(fù)雜算法并且可能需要執(zhí)行系統(tǒng)調(diào)用,所以它通常是比較耗時(shí)的操作。? ?
? ? redis內(nèi)存分配:
1、空間預(yù)分配:如果修改后大小小于1MB,程序分配和len大小一樣的未使用空間,如果修改后大于1MB,程序分配? 1MB的未使用空間。修改長(zhǎng)度時(shí)檢查,夠的話就直接使用未使用空間,不用再分配。?
2、惰性空間釋放:字符串縮短時(shí)不需要釋放空間,用free記錄即可,留作以后使用。
? ? 二進(jìn)制安全
c字符串除了末尾外,不能包含空字符,否則程序讀到空字符會(huì)誤以為是結(jié)尾,這就限制了c字符串只能保存文本,二進(jìn)制文件就不能保存了。
而redis字符串都是二進(jìn)制安全的,因?yàn)橛衛(wèi)en來(lái)記錄長(zhǎng)度。
2 鏈表
作為一種常用數(shù)據(jù)結(jié)構(gòu),鏈表內(nèi)置在很多高級(jí)語(yǔ)言中,因?yàn)閏并沒(méi)有,所以redis實(shí)現(xiàn)了自己的鏈表。
鏈表在redis也有一定的應(yīng)用,比如列表鍵的底層實(shí)現(xiàn)之一就是鏈表。(當(dāng)列表鍵包含大量元素或者元素都是很長(zhǎng)的字符串時(shí))
發(fā)布與訂閱、慢查詢、監(jiān)視器等功能也用到了鏈表。
具體實(shí)現(xiàn):
//redis的節(jié)點(diǎn)使用了雙向鏈表結(jié)構(gòu) typedef struct listNode {// 前置節(jié)點(diǎn)struct listNode *prev;// 后置節(jié)點(diǎn)struct listNode *next;// 節(jié)點(diǎn)的值void *value; } listNode; //其實(shí)學(xué)過(guò)數(shù)據(jù)結(jié)構(gòu)的應(yīng)該都實(shí)現(xiàn)過(guò) typedef struct list {// 表頭節(jié)點(diǎn)listNode *head;// 表尾節(jié)點(diǎn)listNode *tail;// 鏈表所包含的節(jié)點(diǎn)數(shù)量unsigned long len;// 節(jié)點(diǎn)值復(fù)制函數(shù)void *(*dup)(void *ptr);// 節(jié)點(diǎn)值釋放函數(shù)void (*free)(void *ptr);// 節(jié)點(diǎn)值對(duì)比函數(shù)int (*match)(void *ptr, void *key); } list;總結(jié)一下redis鏈表特性:
雙端、無(wú)環(huán)、帶長(zhǎng)度記錄、
多態(tài):使用?void*?指針來(lái)保存節(jié)點(diǎn)值, 可以通過(guò)?dup?、?free?、?match?為節(jié)點(diǎn)值設(shè)置類型特定函數(shù), 可以保存不同類型的值。
3、字典
其實(shí)字典這種數(shù)據(jù)結(jié)構(gòu)也內(nèi)置在很多高級(jí)語(yǔ)言中,但是c語(yǔ)言沒(méi)有,所以redis自己實(shí)現(xiàn)了。
應(yīng)用也比較廣泛,比如redis的數(shù)據(jù)庫(kù)就是字典實(shí)現(xiàn)的。不僅如此,當(dāng)一個(gè)哈希鍵包含的鍵值對(duì)比較多,或者都是很長(zhǎng)的字符串,redis就會(huì)用字典作為哈希鍵的底層實(shí)現(xiàn)。
來(lái)看看具體是實(shí)現(xiàn):
//redis的字典使用哈希表作為底層實(shí)現(xiàn) typedef struct dictht {// 哈希表數(shù)組dictEntry **table;// 哈希表大小unsigned long size;// 哈希表大小掩碼,用于計(jì)算索引值// 總是等于 size - 1unsigned long sizemask;// 該哈希表已有節(jié)點(diǎn)的數(shù)量unsigned long used;} dictht;table?是一個(gè)數(shù)組, 數(shù)組中的每個(gè)元素都是一個(gè)指向dictEntry?結(jié)構(gòu)的指針, 每個(gè)?dictEntry?結(jié)構(gòu)保存著一個(gè)鍵值對(duì)。
圖為一個(gè)大小為4的空哈希表。
我們接著就來(lái)看dictEntry的實(shí)現(xiàn):
typedef struct dictEntry {// 鍵void *key;// 值union {void *val;uint64_t u64;int64_t s64;} v;// 指向下個(gè)哈希表節(jié)點(diǎn),形成鏈表struct dictEntry *next; } dictEntry;(v可以是一個(gè)指針, 或者是一個(gè)?uint64_t?整數(shù), 又或者是一個(gè)?int64_t?整數(shù)。)
next就是解決鍵沖突問(wèn)題的,沖突了就掛后面,這個(gè)學(xué)過(guò)數(shù)據(jù)結(jié)構(gòu)的應(yīng)該都知道吧,不說(shuō)了。
?
下面我們來(lái)說(shuō)字典是怎么實(shí)現(xiàn)的了。
typedef struct dict {// 類型特定函數(shù)dictType *type;// 私有數(shù)據(jù)void *privdata;// 哈希表dictht ht[2];// rehash 索引int rehashidx; //* rehashing not in progress if rehashidx == -1 } dict;type?和?privdata?是對(duì)不同類型的鍵值對(duì), 為創(chuàng)建多態(tài)字典而設(shè)置的:
type?指向?dictType?, 每個(gè)?dictType?保存了用于操作特定類型鍵值對(duì)的函數(shù), 可以為用途不同的字典設(shè)置不同的類型特定函數(shù)。
而?privdata?屬性則保存了需要傳給那些類型特定函數(shù)的可選參數(shù)。
而dictType就暫時(shí)不展示了,不重要而且字有點(diǎn)多。。。還是講有意思的東西吧? ? rehash(重新散列)
隨著我們不斷的操作,哈希表保存的鍵值可能會(huì)增多或者減少,為了讓哈希表的負(fù)載因子維持在合理的范圍內(nèi),有時(shí)需要對(duì)哈希表進(jìn)行合理的擴(kuò)展或者收縮。?一般情況下, 字典只使用?ht[0]?哈希表,?ht[1]?哈希表只會(huì)在對(duì)?ht[0]?哈希表進(jìn)行 rehash 時(shí)使用。
redis字典哈希rehash的步驟如下:
1)為ht[1]分配合理空間:如果是擴(kuò)展操作,大小為第一個(gè)大于等于ht[0]*used*2的,2的n次冪。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?如果是收縮操作,大小為第一個(gè)大于等于ht[0]*used的,2的n次冪。
2)將ht[0]中的數(shù)據(jù)rehash到ht[1]上。
3)釋放ht[0],將ht[1]設(shè)置為ht[0],ht[1]創(chuàng)建空表,為下次做準(zhǔn)備。
? ? 漸進(jìn)rehash
數(shù)據(jù)量特別大時(shí),rehash可能對(duì)服務(wù)器造成影響。為了避免,服務(wù)器不是一次性rehash的,而是分多次。
我們維持一個(gè)變量rehashidx,設(shè)置為0,代表rehash開(kāi)始,然后開(kāi)始rehash,在這期間,每個(gè)對(duì)字典的操作,程序都會(huì)把索引rehashidx上的數(shù)據(jù)移動(dòng)到ht[1]。
隨著操作不斷執(zhí)行,最終我們會(huì)完成rehash,設(shè)置rehashidx為-1.
需要注意:rehash過(guò)程中,每一次增刪改查也是在兩個(gè)表進(jìn)行的。
?
總結(jié)
以上是生活随笔為你收集整理的redis——数据结构(字典、链表、字符串)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 《Head First 设计模式》第十章
- 下一篇: Date类(日期时间类)219