Redis爲什麼這麼快--關鍵在於它的數據結構

 本文內容思惟導圖以下:redis

 

 

 

 

1、簡介和應用算法


Redis是一個由ANSI C語言編寫,性能優秀、支持網絡、可持久化的K-K內存數據庫,並提供多種語言的API。它經常使用的類型主要是 String、List、Hash、Set、ZSet 這5種數據庫

 

 

 

 

Redis在互聯網公司通常有如下應用:數組

  • String:緩存、限流、計數器、分佈式鎖、分佈式Session
  • Hash:存儲用戶信息、用戶主頁訪問量、組合查詢
  • List:微博關注人時間軸列表、簡單隊列
  • Set:贊、踩、標籤、好友關係
  • Zset:排行榜

再好比電商在大促銷時,會用一些特殊的設計來保證系統穩定,扣減庫存能夠考慮以下設計:緩存

 

 

 

 

上圖中,直接在Redis中扣減庫存,記錄日誌後經過Worker同步到數據庫,在設計同步Worker時須要考慮併發處理和重複處理的問題。安全

經過上面的應用場景能夠看出Redis是很是高效和穩定的,那Redis底層是如何實現的呢?性能優化

 

 

 

 

 

 

2、Redis的對象redisObject網絡


 

當咱們執行set hello world命令時,會有如下數據模型:數據結構

 

 

 

 

 

  • dictEntry:Redis給每一個key-value鍵值對分配一個dictEntry,裏面有着key和val的指針,next指向下一個dictEntry造成鏈表,這個指針能夠將多個哈希值相同的鍵值對連接在一塊兒,由此來解決哈希衝突問題(鏈地址法)。
  • sds:鍵key「hello」是以SDS(簡單動態字符串)存儲,後面詳細介紹。
  • redisObject:值val「world」存儲在redisObject中。實際上,redis經常使用5中類型都是以redisObject來存儲的;而redisObject中的type字段指明瞭Value對象的類型,ptr字段則指向對象所在的地址。

redisObject對象很是重要,Redis對象的類型、內部編碼、內存回收、共享對象等功能,都須要redisObject支持。這樣設計的好處是,能夠針對不一樣的使用場景,對5中經常使用類型設置多種不一樣的數據結構實現,從而優化對象在不一樣場景下的使用效率。架構

不管是dictEntry對象,仍是redisObject、SDS對象,都須要內存分配器(如jemalloc)分配內存進行存儲。jemalloc做爲Redis的默認內存分配器,在減少內存碎片方面作的相對比較好。好比jemalloc在64位系統中,將內存空間劃分爲小、大、巨大三個範圍;每一個範圍內又劃分了許多小的內存塊單位;當Redis存儲數據時,會選擇大小最合適的內存塊進行存儲。

前面說過,Redis每一個對象由一個redisObject結構表示,它的ptr指針指向底層實現的數據結構,而數據結構由encoding屬性決定。好比咱們執行如下命令獲得存儲「hello」對應的編碼:

 

 

 

 

redis全部的數據結構類型以下(重要,後面會用):

 

 

 

 

3、String


 

字符串對象的底層實現能夠是int、raw、embstr(上面的表對應有名稱介紹)。embstr編碼是經過調用一次內存分配函數來分配一塊連續的空間,而raw須要調用兩次。

 

 

 

 

 int編碼字符串對象和embstr編碼字符串對象在必定條件下會轉化爲raw編碼字符串對象。embstr:<=39字節的字符串。int:8個字節的長整型。raw:大於39個字節的字符串。

簡單動態字符串(SDS),這種結構更像C++的String或者Java的ArrayList<Character>,長度動態可變:

1struct sdshdr {
2    // buf 中已佔用空間的長度
3    int len;
4    // buf 中剩餘可用空間的長度
5    int free;
6    // 數據空間
7    char buf[]; // ’\0’空字符結尾
8};

 

  •     get:sdsrange---O(n)
  • set:sdscpy—O(n)
  • create:sdsnew---O(1)
  • len:sdslen---O(1)

常數複雜度獲取字符串長度:由於SDS在len屬性中記錄了長度,因此獲取一個SDS長度時間複雜度僅爲O(1)。

預空間分配:若是對一個SDS進行修改,分爲一下兩種狀況:

  • SDS長度(len的值)小於1MB,那麼程序將分配和len屬性一樣大小的未使用空間,這時free和len屬性值相同。舉個例子,SDS的len將變成15字節,則程序也會分配15字節的未使用空間,SDS的buf數組的實際長度變成15+15+1=31字節(額外一個字節用戶保存空字符)。
  • SDS長度(len的值)大於等於1MB,程序會分配1MB的未使用空間。好比進行修改以後,SDS的len變成30MB,那麼它的實際長度是30MB+1MB+1byte。

惰性釋放空間:當執行sdstrim(截取字符串)以後,SDS不會立馬釋放多出來的空間,若是下次再進行拼接字符串操做,且拼接的沒有剛纔釋放的空間大,則那些未使用的空間就會排上用場。經過惰性釋放空間避免了特定狀況下操做字符串的內存從新分配操做。

杜絕緩衝區溢出:使用C字符串的操做時,若是字符串長度增長(如strcat操做)而忘記從新分配內存,很容易形成緩衝區的溢出;而SDS因爲記錄了長度,相應的操做在可能形成緩衝區溢出時會自動從新分配內存,杜絕了緩衝區溢出。

4、List


 

List對象的底層實現是quicklist(快速列表,是ziplist 壓縮列表 和linkedlist 雙端鏈表 的組合)。Redis中的列表支持兩端插入和彈出,並能夠得到指定位置(或範圍)的元素,能夠充當數組、隊列、棧等。

1typedef struct listNode {
 2     // 前置節點
 3    struct listNode *prev;
 4    // 後置節點
 5    struct listNode *next;
 6    // 節點的值
 7    void *value;
 8 } listNode;
 9
10 typedef struct list {
11     // 表頭節點
12    listNode *head;
13    // 表尾節點
14    listNode *tail;
15    // 節點值複製函數
16    void *(*dup)(void *ptr);
17    // 節點值釋放函數
18    void (*free)(void *ptr);
19     // 節點值對比函數
20    int (*match)(void *ptr, void *key);
21     // 鏈表所包含的節點數量
22    unsigned long len;
23 } list;

 

  •   rpush: listAddNodeHead ---O(1)
  • lpush: listAddNodeTail ---O(1)
  • push:listInsertNode ---O(1)
  • index : listIndex ---O(N)
  • pop:ListFirst/listLast ---O(1)
  • llen:listLength ---O(N)

 

4.1 linkedlist(雙端鏈表)

此結構比較像Java的LinkedList,有興趣能夠閱讀一下源碼。

 

 

 

 

 

從圖中能夠看出Redis的linkedlist雙端鏈表有如下特性:節點帶有prev、next指針、head指針和tail指針,獲取前置節點、後置節點、表頭節點和表尾節點的複雜度都是O(1)。len屬性獲取節點數量也爲O(1)

與雙端鏈表相比,壓縮列表能夠節省內存空間,可是進行修改或增刪操做時,複雜度較高;所以當節點數量較少時,可使用壓縮列表;可是節點數量多時,仍是使用雙端鏈表划算。

 

4.2 ziplist(壓縮列表)

當一個列表鍵只包含少許列表項,且是小整數值或長度比較短的字符串時,那麼redis就使用ziplist(壓縮列表)來作列表鍵的底層實現。

 

 

 

 

 

ziplist是Redis爲了節約內存而開發的,是由一系列特殊編碼的連續內存塊(而不是像雙端鏈表同樣每一個節點是指針)組成的順序型數據結構;具體結構相對比較複雜,有興趣讀者能夠看 Redis 哈希結構內存模型剖析。在新版本中list鏈表使用 quicklist 代替了 ziplist和 linkedlist

 

 

 

 

quickList 是 zipList 和 linkedList 的混合體。它將 linkedList 按段切分,每一段使用 zipList 來緊湊存儲,多個 zipList 之間使用雙向指針串接起來。由於鏈表的附加空間相對過高,prev 和 next 指針就要佔去 16 個字節 (64bit 系統的指針是 8 個字節),另外每一個節點的內存都是單獨分配,會加重內存的碎片化,影響內存管理效率。

 

 

 

 

quicklist 默認的壓縮深度是 0,也就是不壓縮。爲了支持快速的 push/pop 操做,quicklist 的首尾兩個 ziplist 不壓縮,此時深度就是 1。爲了進一步節約空間,Redis 還會對 ziplist 進行壓縮存儲,使用 LZF 算法壓縮。

 

5、Hash


 

Hash對象的底層實現能夠是ziplist(壓縮列表)或者hashtable(字典或者也叫哈希表)。

 

 

 

 

Hash對象只有同時知足下面兩個條件時,纔會使用ziplist(壓縮列表):1.哈希中元素數量小於512個;2.哈希中全部鍵值對的鍵和值字符串長度都小於64字節。

hashtable哈希表能夠實現O(1)複雜度的讀寫操做,所以效率很高。源碼以下

1typedef struct dict {
 2    // 類型特定函數
 3    dictType *type;
 4     // 私有數據
 5    void *privdata;
 6     // 哈希表
 7    dictht ht[2];
 8    // rehash 索引
 9    // 當 rehash 不在進行時,值爲 -1
10    int rehashidx; /* rehashing not in progress if rehashidx == -1 */
11     // 目前正在運行的安全迭代器的數量
12    int iterators; /* number of iterators currently running */
13 } dict;
14 typedef struct dictht {
15    // 哈希表數組
16    dictEntry **table;
17     // 哈希表大小
18    unsigned long size;
19    // 哈希表大小掩碼,用於計算索引值
20    // 老是等於 size - 1
21    unsigned long sizemask;
22    // 該哈希表已有節點的數量
23    unsigned long used;
24} dictht;
25typedef struct dictEntry {
26    void *key;
27    union {void *val;uint64_t u64;int64_t s64;} v;
28    // 指向下個哈希表節點,造成鏈表
29    struct dictEntry *next;
30 } dictEntry;
31 typedef struct dictType {
32     // 計算哈希值的函數
33    unsigned int (*hashFunction)(const void *key);
34     // 複製鍵的函數
35    void *(*keyDup)(void *privdata, const void *key);
36     // 複製值的函數
37    void *(*valDup)(void *privdata, const void *obj);
38     // 對比鍵的函數
39    int (*keyCompare)(void *privdata, const void *key1, const void *key2);
40    // 銷燬鍵的函數
41    void (*keyDestructor)(void *privdata, void *key);
42    // 銷燬值的函數
43    void (*valDestructor)(void *privdata, void *obj);
44} dictType;

上面源碼能夠簡化成以下結構:

 

 

 

 

 

這個結構相似於JDK7之前的HashMap<String,Object>,當有兩個或以上的鍵被分配到哈希數組的同一個索引上時,會產生哈希衝突。Redis也使用鏈地址法來解決鍵衝突。即每一個哈希表節點都有一個next指針,多個哈希表節點用next指針構成一個單項鍊表,鏈地址法就是將相同hash值的對象組織成一個鏈表放在hash值對應的槽位。

Redis中的字典使用hashtable做爲底層實現的話,每一個字典會帶有兩個哈希表,一個平時使用,另外一個僅在rehash(從新散列)時使用。隨着對哈希表的操做,鍵會逐漸增多或減小。爲了讓哈希表的負載因子維持在一個合理範圍內,Redis會對哈希表的大小進行擴展或收縮(rehash),也就是將ht【0】裏面全部的鍵值對分屢次、漸進式的rehash到ht【1】裏

6、Set


 

Set集合對象的底層實現能夠是intset(整數集合)或者hashtable(字典或者也叫哈希表)。

 

 

 

 

intset(整數集合)當一個集合只含有整數,而且元素很少時會使用intset(整數集合)做爲Set集合對象的底層實現。

1typedef struct intset {
2    // 編碼方式
3    uint32_t encoding;
4    // 集合包含的元素數量
5    uint32_t length;
6    // 保存元素的數組
7    int8_t contents[];
8} intset;

 

  • sadd:intsetAdd---O(1)
  • smembers:intsetGetO(1)---O(N)
  • srem:intsetRemove---O(N)
  • slen:intsetlen ---O(1)

intset底層實現爲有序,無重複數組保存集合元素。 intset這個結構裏的整數數組的類型能夠是16位的,32位的,64位的。若是數組裏全部的整數都是16位長度的,若是新加入一個32位的整數,那麼整個16的數組將升級成一個32位的數組。升級能夠提高intset的靈活性,又能夠節約內存,但不可逆。

7.ZSet


 

ZSet有序集合對象底層實現能夠是ziplist(壓縮列表)或者skiplist(跳躍表)。

 

 

 

 

當一個有序集合的元素數量比較多或者成員是比較長的字符串時,Redis就使用skiplist(跳躍表)做爲ZSet對象的底層實現。

1typedef struct zskiplist {
 2     // 表頭節點和表尾節點
 3    struct zskiplistNode *header, *tail;
 4    // 表中節點的數量
 5    unsigned long length;
 6    // 表中層數最大的節點的層數
 7    int level;
 8 } zskiplist;
 9typedef struct zskiplistNode {
10    // 成員對象
11    robj *obj;
12    // 分值
13    double score;
14     // 後退指針
15    struct zskiplistNode *backward;
16    // 層
17    struct zskiplistLevel {
18        // 前進指針
19        struct zskiplistNode *forward;
20         // 跨度---前進指針所指向節點與當前節點的距離
21        unsigned int span;
22    } level[];
23} zskiplistNode;

 

zadd---zslinsert---平均O(logN), 最壞O(N)

zrem---zsldelete---平均O(logN), 最壞O(N)

zrank--zslGetRank---平均O(logN), 最壞O(N)

 

 

 

在此我向你們推薦一個架構學習交流qun。交流學習qun號:+q q-q u n:948 368 769 qun內已經有小夥伴將知識體系整理好(源碼,筆記,PPT,學習視頻)。裏面會分享一些資深架構師錄製的視頻錄像:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構等這些成爲架構師必備的知識體系。還能領取免費的學習資源,目前受益良多

 

skiplist的查找時間複雜度是LogN,能夠和平衡二叉樹至關,但實現起來又比它簡單。跳躍表(skiplist)是一種有序數據結構,它經過在某個節點中維持多個指向其餘節點的指針,從而達到快速訪問節點的目的。

相關文章
相關標籤/搜索