天天看點

多角度剖析redis資料結構及底層實作原理、應用場景1.字元串(string)2.連結清單(list)3.哈希(hash)4.無序集合(set)5 有序集合(zset)6. bitmap定義:7. HyperLogLog定義:8. GEO定義:總結:

目錄

1.字元串(string)

 1.1 redis的字元串類型:

1.2  string的編碼方式有三種:

1.3 string的預配置設定空間機制;

1.4對比C語言的字元串優點:

1.5 string的使用場景:

2.連結清單(list)

2.1 redis3.2版本之前連結清單結構:

2.2  redis3.2版本之前ziplist轉化為linklist條件 :

2.3 linklist結構:

2.3.1 linklist缺點:

2.4 ziplist結構:

2.4.1 ziplist資料結構定義:

2.4.2 ziplist的連鎖更新問題:

2.4.3 ziplist使用場景:

2.5 redis版本3.2後的quicklist(快速連結清單):

2.5.1 quicklist相關結構:

2.5.2 注意quicklist的插入:

2.5.3 quicklist的優點:

2.6 list應用場景:

3.哈希(hash)

3.1 hash資料結構:

3.2 hastable的實作:

3.3 hashtable如何解決hash沖突:

3.4 rehash過程:

3.4.1 觸發條件:

3.4.2 擴容過程:

3.4.3 擴容的意義:

3.5 hash應用場景:

4.無序集合(set)

4.1 set的資料結構:

4.2 使用intset結構的條件:

4.3 intset結構屬性:

4.4 intset更新過程;

4.5 intset優缺點:

4.6 set應用場景:

5 有序集合(zset)

5.1 zset的資料結構:

5.2 使用skiplist的條件:

5.3 skiplist結構:

5.4 skiplist查找過程:

5.5 skiplist的插入過程:

5.6 删除過程:

5.7 修改過程:

5.8 skiplist的優點:

5.9 zset的應用場景:

6. bitmap定義:

6.1 bitmap的優點:

6.2 bitmap的使用場景:

7. HyperLogLog定義:

7.1 HyperLogLog的使用場景:

8. GEO定義:

總結:

redis是由C語言編寫的,是以鍵值對(K-V)的方式來存儲資料,K的資料類型為字元串(string)類型,V的值則為下文中的5種資料類型

1.字元串(string)

 1.1 redis的字元串類型:

是由SDS(simple dynamic string) 來實作的。

結構定義:

struct sdshdr{

int len;  //buff中已使用的空間

int free; //buff中未使用的空間

char buff[]; //資料空間

}

1.2  string的編碼方式有三種:

int(整數類型,直接用redisObject存儲), raw(大于39位元組長度) , embstr(小于等于39位元組長度)(redis 4.0之前的版本)

redisObject{

unsigned type : 4  //string ,list,hash,set,zset(4位 = 0.5位元組)

unsigned econding: 4 //redis内部編碼類型,如:hashtable,sds,ziplist skiplist等 (4位 = 0.5位元組)

unsigned lru ://記錄對象最後一次被通路的時間(24位 = 3位元組)

int refcount: //用來記錄被使用的次數,當值為0時,則可回收空間 (32位 = 4位元組)

void *pre: //如果是int類型 則存儲值,如果是其它類型則存儲指向資料的指針(8位元組)

}

使用重點提示:在高并發的情況下,盡量使其長度小于等于39位元組,采用embstr類型,字元串SDS和redisObject一起配置設定(同時建立redisObject,sdshdr對象,配置設定空間連續),進而隻需要一次記憶體操作(raw需要2次),減少建立redisObject的記憶體配置設定次數來提高性能

知識點提示:39位元組長度 = embstr最小配置設定64位元組 - len free 8位元組 - /0 1個位元組(結束字元) - redisObject(16位元組)(

版本提示:在redis 5.0 後39位元組 -> 44 位元組,優化了SDS小記憶體使用,因為unsing int 可以表示很大的範圍,但是對于很短的sds則浪費了2個unsigned int(8個位元組的空間)

優化後為: struct __attribute__((_packed__)) sdshdr8(還有16,32, 64的使用){

uint8_t len;

uint8_t alloc;

unsigned char flags;

char buff [];

}

知識點提示:44位元組 = (變動)原來的len 8個位元組 - (uint8_t len uint8_t alloc unsigned char flags)3個位元組 + 39位元組

1.3 string的預配置設定空間機制;

     在修改SDS時,會先檢查空間是否足夠,不足的會盡行擴容,防止緩沖區溢出。

     擴容:當小于1M的時候,采用空間翻倍+1M,當大于1M,則每次+1M,上限為512M

     縮短的時候,也不會立即縮短,而是采用惰性釋放空間,采用對應的API來釋放

1.4對比C語言的字元串優點:

使用起來更友善,C語言要碰到結束/0标志才用停止。

防止記憶體空間溢出,在使用之前會檢查空間大小

預配置設定空間機制

1.5 string的使用場景:

緩存,session共享,限流,計數器

2.連結清單(list)

2.1 redis3.2版本之前連結清單結構:

ziplist(壓縮清單),linklist(雙向連結清單),redis3.2版本後使用quicklist(快速連結清單)

建立新清單時預設優先采用ziplist,節省記憶體空間

2.2  redis3.2版本之前ziplist轉化為linklist條件 :

一:新添的字元串長度大于64位元組。二:鍵值對數量大于512,則會由redis_encoding_ziplist編碼轉換成redis_encoding_linklist編碼。以上2個條件的值在redis.conf中 list-max-ziplist-value 64     list-max-ziplist-entries 512可更改

2.3 linklist結構:

linklist是雙向連結清單,Node節點包含prev,next指針,使其可以雙向周遊,同時list還儲存了head,tail兩個指針,使其對于連結清單頭部與尾部的進行插入的操作複雜度都為O(1),使用lpush,rpop指令具有高效性

連結清單節點:

struct listNode{

struct listNode *prev; //前置指針

struct listNode *next;//後置指針

void *value;//節點的值

}listNode;

連結清單:

struct list{

lsitNode *head;//表頭節點

listNode *tail; //表尾結點

unsigned long len; //連結清單節點數量

void *(*dup)(void *ptr);//節點值複制函數

void (*free)(void *ptr);//節點值釋放函數

int (*match)(void *ptr,void *key); //節點值對比函數

}

2.3.1 linklist缺點:

附加空間太高,prev next指針就占了16個位元組,每個節點的記憶體都是單獨配置設定的,容易産生記憶體碎片

2.4 ziplist結構:

是特殊編碼的連續記憶體塊組成的順序存儲結構,包含多個節點(entry),每個節點都可以用來存儲一個整數或者一個字元串。

ziplist是通過上一個entry的長度 + 目前entry的長度來推算下一個entry的位置,犧牲讀性能,來擷取更低的記憶體空間的使用,存儲短字元串存儲指針比存儲entry長度更費記憶體,典型的“時間換空間”

2.4.1 ziplist資料結構定義:

zlbytes:unit32_t zilplist的長度(位元組)是一個32位的無符号整數,表示整個ziplist占用的記憶體位元組數,對ziplist進行記憶體重配置設定跟計算末端時使用

zltail:unit32_t 到達ziplist表尾結點的偏移量,通過這個偏移量,可以直接彈出表尾結點。

zilen:unit16_t iplist節點的數量,當值小于UINT16_MAX(65535) 就是節點數量,等于時,需要周遊整個ziplist才能計算出

entry: 節點 ,長度由内容決定 =zlentry

zllend‘:标記ziplist的末端

知識點提示:entries才是存儲節點的區域

typedef struct zlentry{

unsigned int preverawlensize,prevrawlen;// prevrawlen表示前一個節點的長度,preranlensize表示prevrawlen長度用1或者5位元組表示

unsigned int lensize, len ;//len表示目前節點的長度 lensize表示 目前節點所需位元組大小

unsigned int headersize// 表示目前節點header大小

unsigned char encoding;//節點的編碼方式

unsigned char *p//指向節點的指針

}zlentry

如何通過一個節點找到另一個節點? 通過使用目前指針-前一個entry的長度,就可以擷取指向另一個節點的位址

zlentry中屬性值的作用:

prevrawlen:記錄前一個節點的所占用的位元組數,可以計算目前節點的前一個節點的位址,可以用來表尾向表頭節點周遊。

len/encoding:記錄目前content占有的記憶體位元組數及存儲類型,用來解析content

content:儲存了目前節點的值

prevrawlen:如果前一個節點prevranlen長度小于254位元組 則值為1。如果大于等于,那麼 将第一個位元組設定為254,後4個位元組用來表示實際長度

2.4.2 ziplist的連鎖更新問題:

在ziplist中,zlentry都會儲存上一個節點的長度prevrawlen,假設新插入了一個節點長度254,而下一個節點的長度為253,就會導緻由原來給的1位元組變為5位元組,使得下一個節點的整體長度發生改變,進而又影響到了下下節點,以此類似,會導緻想多米諾骨牌一樣發生連鎖更新反應,産生空間再配置設定。

2.4.3 ziplist使用場景:

資料量小的情況下,不适合修改操作。

2.5 redis版本3.2後的quicklist(快速連結清單):

quicklsit實際上是ziplist跟linklist的混合體,結合了雙方的優點,是一個ziplist組成的雙向連結清單,quicklist分成N個quicklistNode中包含ziplist,prev,next.

多角度剖析redis資料結構及底層實作原理、應用場景1.字元串(string)2.連結清單(list)3.哈希(hash)4.無序集合(set)5 有序集合(zset)6. bitmap定義:7. HyperLogLog定義:8. GEO定義:總結:

                                                               未壓縮時結構圖

多角度剖析redis資料結構及底層實作原理、應用場景1.字元串(string)2.連結清單(list)3.哈希(hash)4.無序集合(set)5 有序集合(zset)6. bitmap定義:7. HyperLogLog定義:8. GEO定義:總結:

壓縮後(LZF算法)的結構圖

2.5.1 quicklist相關結構:

typedef struct quicklist {

quicklistNode *head; //指向頭節點的指針

quicklistNode *tail;//指向尾結點的指針

unsigned long count;//所有ziplist資料的個數總和

unsigned  long len;//quicklist節點個數

int fill : 16;//ziplist中entry能儲存的數量,由list-max-ziplist-size配置項控制

unsigned int compress : 16//壓縮深度,由list-compress-depth配置項控制

}quicklist;

typedef struct quicklistNode{

struct quicklistNode *prev;//前節點指針

struct quicklistNode *next;//後節點指針

unsigned char *zl;//資料指針,目前節點如果沒有壓縮則指向ziplist結構,如果壓縮了,則指向quicklistLZF;

unsigned int sz; //指向ziplist實際占用的記憶體大小,即使壓縮了也是指向壓縮前的大小

unsigned int count:16 ;// ziplist包含的資料項個數

unsigned int encoding:2; //ziplist是否壓縮  1 :ziplist , 2:quicklistLZF

unsigned int container:2;//存儲類型,2表示使用ziplist存儲

unsigned int recompress : 1; //檢視已經壓縮後的資料,用來标記暫時解壓,後期再進行壓縮

unsigned int attemped_compress:1; 

unsigned int extra :10; 

}quciklistNode;

typedef struct quicklistLZF{

unsigned int sz; //壓縮後的ziplist大小

char compressed[];//柔性數組,用來存放壓縮後的ziplist位元組數組

}quicklistLZF;

2.5.2 注意quicklist的插入:

當要插入的資料沒有超過限制的大小時,則直接插入

當插入的資料大于要插入的ziplist位置時,如果相鄰的quicklist連結節點的ziplist沒有超過限制大小那麼就轉為插入相鄰quicklistNode的ziplist中,

當插入的資料大于要插入的ziplist位置時,如果相鄰的quicklist連結節點的ziplist也超過限制大小時,則建立一個quicklistNode插入

2.5.3 quicklist的優點:

中和了linklist,ziplist的優點,進一步的壓縮了清單,提高了性能。

2.6 list應用場景:

消息隊列,排行榜,評論清單,點贊清單

3.哈希(hash)

3.1 hash資料結構:

hashtable ,ziplist(上文已有說明)

當哈希對象儲存的鍵值對小于512,字元串長度小于64位元組時,使用ziplist,反之則使用hashtable

3.2 hastable的實作:

是由一個dict的結構體,類似hashmap中的資料結構

typedef struct dict {

dictType *type; //類型函數

void *privdate;//私有資料

dictht ht[2]; //哈希表

int rehashidx//rehash索引 當rehash不在進行時 = -1 ,正在進行時為0

} dict;

struct dictht{

dictEntry **table; //哈希數組

unsigned long size; //哈希表大小

unsigned long sizemask;/掩碼,用于計算索引值 = size -1

unsigned long used;//已有節點數量

}dictht;

struce dictEntry{

void *key; //key值

union{  //value值

void *val; //指針

unit64_tu64; //整數

int64_ts64;//整數

}v;

struct dictEntry *next; //下一個節點的指針

}dictEntry;

3.3 hashtable如何解決hash沖突:

取決于hash函數,需要均勻分布。redis預設的函數是siphash,均勻分布,性能佳。當出現hash沖突時,會采用數組+連結清單的方式來解決,被分到同一個哈希桶上的多個哈希表節點用next指針構成一個單連結清單,時間複雜度O(n)。

3.4 rehash過程:

3.4.1 觸發條件:

負載因子 = 哈希表已儲存的節點數量/哈希表大小

load_factor = ht[0].used/ ht[0].size

   一,當負載因子大于等于1時,并且沒有執行bgsave或者bgrewriteaof指令,即沒有執行RDB快照或者沒有進行AOF的重寫。二:當負載因子大于等于5時,就會強制rehash

提示:當負載因子小于0.1時,執行收縮操作

3.4.2 擴容過程:

在将ht[0]的所有鍵值對移到ht[1]中時,采用的漸進式的rehash,防止資料量大的時候,會影響伺服器的使用。

在擴容期間,dict.rehashidx= 0,删改查的操作會同時作用在ht[0],ht[1]兩張表中,查找時會優先查找ht[0],找不到就到ht[1]中找,增加的操作隻會出現在ht[1]中,随着rehsh操作,将dict.dictht[0].table中的每個dictEntry重新計算哈希值跟索引值,移到dict.dictht[1].table中,知道dict.dictht[0].used = 0時,表示rehash完畢.ht[0]會變成空表,并将ht[1]設定為ht[0],重新建立一個ht[1],為下一次擴容做準備,rehashidx值變為-1.

擴容大小:當初始化,dict.dictht[0].table時,會将容量增大到 DICT_HT_INITIAL_SIZE會擴容到4個,其它情況,會擴容到dict.dictht[0].used*2的2^n

3.4.3 擴容的意義:

在哈希沖突增多的情況下,時間複雜度由O(1) -> O(n),擴容的意義就是将時間複雜度逐漸趨于O(1)

3.5 hash應用場景:

使用者資訊,購物車

4.無序集合(set)

4.1 set的資料結構:

hashset(上文已有說明) intset

4.2 使用intset結構的條件:

集合儲存的所有元素都是整數值,儲存的元素數量不超過512個

4.3 intset結構屬性:

typedef struct intset{

unit32_t conding;//編碼方式 三種:INTSET_ENC_INT16 INTSET_ENC_INT32 INTSET_ENC_INT64

unit32_length;//集合包含的元素數量

int8_t contents[];  //儲存元素的數組,從小到大有序排列,不包含重複值 ,時間複雜度為O(1)       

}intset;

提示:intset存儲的資料是有序的,查找的時候是通過二分查找來實作的

4.4 intset更新過程;

當要新增的元素已經超過設定的編碼方式時,就發生整數的更新過程.

首先了解舊的編碼方式,

然後确定新的編碼方式計算出要新增的記憶體大小,将新元素從尾部插入,

根據新的編碼方式,将之前的值充值,contents存在2種編碼格式設定的值,需要統一,從新插入的元素位置開始,從後向前(防止資料被覆寫)将之前的資料按照新的編碼方式來移動和設定。

4.5 intset優缺點:

優點:節省記憶體,隻在需要更新的時候進行更新操作 缺點:更新過程耗費資源,而且不支援降級

4.6 set應用場景:

交集,并集,差集,交友圈,社交圈,點贊,收藏

5 有序集合(zset)

5.1 zset的資料結構:

ziplist(上文已有說明)  skiplist(跳表)

5.2 使用skiplist的條件:

有序集合的元素數量小于512,有序集合的元素長度小于64位元組時使用ziplist,否者使用skiplist

5.3 skiplist結構:

是一個有序的資料結構。它通過在每個節點中維持多個指向其它節點的指針,進而打到快速通路節點的目的,類似于二分查找,時間複雜度最快O(logn),最慢0(N)。

typedef struct zskiplist{

struct zskiplistNode *header,*tail;//頭尾指針

unsigned long length;//節點數量

int level;//表中最大的節點的層數

}zskiplist

提示:可以通過以上屬性來快速找到頭尾節點,最大節點層數,實作時間負責度O(1)

type struct zskiplistNode{

sds ele;//節點存儲的具體指

double score;//節點對應的分值,節點都按照分值從小到大排序

struct zskiplistNode *backward//前向指針

struct zskiplistLevel{

struct zskiplistNode *forward; //每一層的後向指針

unsigned long span;//到下一個節點的跨度

}level[]; //雙向連結清單

}zskiplistNode;

提示:多個節點的分值可以相同,但是節點的成員對象必須唯一,然後按照對象的大小進行排序,對象小的排在前面(靠近表頭的方向),對象大的排在後面(靠在表尾的方向)。

多角度剖析redis資料結構及底層實作原理、應用場景1.字元串(string)2.連結清單(list)3.哈希(hash)4.無序集合(set)5 有序集合(zset)6. bitmap定義:7. HyperLogLog定義:8. GEO定義:總結:

資料結構圖

5.4 skiplist查找過程:

采用連結清單是想要保值修改時間複雜度低,那麼查找過程的時間複雜度如何優化呢?故而出現了跳表,類似于二分查找的實作思想。隻要保證整體上二分之一的機率就行,不用保證兩兩層級之間二分之一的機率

假設要找score為5的元素。

(1)首先從頭結點的最高層(也就是64層)開始查找,但是由于頭指針中儲存了目前跳表的最高層數,是以直接從頭結點的level層開始查找即可。如果zkiplistLevel的forward為NULL,則繼續向下。

(2)直到頭結點中第5層的zskiplistLevel的forward不為空,它指向了第6個節點的第5層,但是這個節點的score為6,大于5,是以還要從頭結點向低層繼續查找。

(3)頭結點中第4層的zskiplistLevel的forward指向第3個節點的第4層,這個節點的score為3,小于5,并且第一個節點的span=2,這個span的作用就是用來累加并計算最終目标節點的排名的。繼續從這個節點的forward向後周遊。它的forward指向第6個節點的第4層,這個節點的score為6,大于5,是以還要從前一個score為3的節點向低層繼續查找。

(4)第3個節點第3層的zskiplistLevel的forward指向第5個節點的第3層,而它的score正好為5,查找成功。并且第3個節點的span=2,再加上原來的span=2等于4,是以目标節點5的排名是4。

多角度剖析redis資料結構及底層實作原理、應用場景1.字元串(string)2.連結清單(list)3.哈希(hash)4.無序集合(set)5 有序集合(zset)6. bitmap定義:7. HyperLogLog定義:8. GEO定義:總結:

5.5 skiplist的插入過程:

(1)插入節點時,會随機配置設定一個小于64的層數,層數越小機率越大,層數越高機率越小

(2)查找小于待插入score分數值中最大的那個節點,如果score相同,則比較對象大小

(3)在找到的節點後面插入新的節點,同時更新前面節點的跨度跟跳表的最大層高

  (4) 更新新節點的各層forward和backward指針

5.6 删除過程:

(1)(2)找位置方式同上

(3)找到待删除的節點,删除節點,更新前面節點的跨度跟跳表最大層高

(4)更新被删除的節點各層的forward和backward指針

5.7 修改過程:

找到要修改的節點位置,直接删除,然後插入修改後的新的節點

5.8 skiplist的優點:

并不是特别耗記憶體,隻要通過調整節點到更高level的機率,就能做到比B樹更少的記憶體消耗

sorted set可能會面對大量的zrange和zreverange操作,跳表作為單連結清單周遊的實作性能亞于其它平衡樹

實作和調試更簡單 

5.9 zset的應用場景:

實時排行榜

6. bitmap定義:

 -又稱位圖,以位為機關的存儲,内部還是采用String類型(最大長度為512M)存儲(上限為2^32),可以了解為一個bit數組來存儲特定資料的一種資料結構

6.1 bitmap的優點:

節省記憶體,适合處理大資料。可以實作快速排序,去重,快速查詢是否存在(布隆過濾器),能友善的通過位預算(AND,OR,XOR,NOT)高效對多個bitmap資料進行處理

6.2 bitmap的使用場景:

活躍使用者線上狀态,活躍使用者統計,使用者簽到等場景,适合大量的使用者,幾千萬上億級别

實時分析,存儲與對象ID相關的資訊

7. HyperLogLog定義:

-估算基數統計的算法,是LogLog算法的更新,在redis中實作HyperLogLog,隻需要12k記憶體就能統計2^64個資料,存在一定的誤差,誤差率在0.81%

7.1 HyperLogLog的使用場景:

可以用來統計頁面實時UV,注冊IP數,每日通路IP數,

8. GEO定義:

-資訊定位功能,支援存儲地理位置資訊,實作諸如附近位置、搖一搖這類依賴于地理位置資訊的功能

總結:

根據自己的需要查閱多方資料,參考多為大佬的心得所寫,記錄下來,以便後面随時再複習一遍。

繼續閱讀