Redis底層原理 (Redis底層數據結構)(一)

Redis底層數據結構

目標:
  • 掌握Redis數據類型的底層數據結構
  • 理解LRU
  • 能夠編寫Redis事務處理,理解弱事務
  • 理解Redis樂觀鎖及秒殺的實現

Redis內存模型
    Redis內存統計
127.0.0.1:6379># Memoryinfo memory
#Redis分配的內存總量,包括虛擬內存(字節)
used_memory:853464
#佔操作系統的內存,不包括虛擬內存(字節)
used_memory_rss:12247040
#內存碎片比例 如果小於0說明使用了虛擬內存
mem_fragmentation_ratio:15.07
#Redis使用的內存分配器
mem_allocator:jemalloc-5.1.0

 Redis內存分配
     數據:作爲數據庫,數據是最主要的部分;這部分佔用的內存會統計在 used_memory 中。Redis 使用鍵值對存儲數據,其中的值(對象)包括 5 種類型,即字符串、哈希、列表、集合、有序集合。這 5 種類型是 Redis 對外提供的,實際上,在 Redis 內部,每種類型可能有 2 種或更多的內部編碼實現
     進程: Redis 主進程本身運行肯定需要佔用內存,如代碼、常量池等等;這部分內存大約幾M,在大多數生產環境中與 Redis 數據佔用的內存相比可以忽略。這部分內存不是由 jemalloc 分配,因此不會統計在 used_memory 中。補充說明:除了主進程外,Redis 創建的子進程運行也會佔用內存,如 Redis 執行 AOF、RDB 重寫時創建的子進程。當然,這部分內存不屬於 Redis 進程,也不會統計在 used_memory 和 used_memory_rss 中。
     緩衝內存:緩衝內存包括客戶端緩衝區、複製積壓緩衝區、AOF 緩衝區等;其中,客戶端緩衝區存儲客戶端連接的輸入輸出緩衝;複製積壓緩衝區用於部分複製功能;AOF 緩衝區用於在進行 AOF 重寫時,保存最近的寫入命令。在瞭解相應功能之前,不需要知道這些緩衝的細節;這部分內存由 jemalloc 分配,因此會統計在used_memory 中。
     內存碎片:內存碎片是 Redis 在分配、回收物理內存過程中產生的。例如,如果對數據的更改頻繁,而且數據之間的大小相差很大,可能導致 Redis 釋放的空間在物理內存中並沒有釋放。但 Redis 又無法有效利用,這就形成了內存碎片,內存碎片不會統計在 used_memory 中。內存碎片的產生與對數據進行的操作、數據的特點等都有關;此外,與使用的內存分配器也有關係:如果內存分配器設計合理,可以儘可能的減少內存碎片的產生。如果 Redis 服務器中的內存碎片已經很大,可以通過安全重啓的方式減小內存碎片:因爲重啓之後,Redis 重新從備份文件中讀取數據,在內存中進行重排,爲每個數據重新選擇合適的內存單元,減小內存碎片。

Redis數據結構
 
Redis 沒有直接使用 C 字符串(即以空字符’\0’結尾的字符數組)作爲默認的字符串表示,而是使用了SDS。SDS 是簡單動態字符串(Simple Dynamic String)的縮寫。它是自己構建了一種名爲 簡單動態字符串(simple dynamic string,SDS)的抽象類型,並將 SDS 作爲Redis的默認字符串表示。
簡單動態字符串(simple dynamic string,SDS)定義:
struct sdshdr{
//記錄buf數組中已使用字節的數量
//等於 SDS 保存字符串的長度
int len;
//記錄 buf 數組中未使用字節的數量
int free;
//字節數組,用於保存字符串
char buf[];
}

buf數組的長度=free+len+1

好處:

SDS 在 C 字符串的基礎上加入了 free 和 len 字段,帶來了很多好處:

獲取字符串長度:SDS 是 O(1),C 字符串是 O(n)。

緩衝區溢出:使用 C 字符串的 API 時,如果字符串長度增加(如 strcat 操作)而忘記重新分配內存,很容易造成緩衝區的溢出。而 SDS 由於記錄了長度,相應的 API 在可能造成緩衝區溢出時會自動重新分配內存,杜絕了緩衝區溢出。

修改字符串時內存的重分配:對於 C 字符串,如果要修改字符串,必須要重新分配內存(先釋放再申請),因爲如果沒有重新分配,字符串長度增大時會造成內存緩衝區溢出,字符串長度減小時會造成內存泄露。而對於 SDS,由於可以記錄 len 和 free,因此解除了字符串長度和空間數組長度之間的關聯,可以在此基礎上進行優化。空間預分配策略(即分配內存時比實際需要的多)使得字符串長度增大時重新分配內存的概率大大減小;惰性空間釋放策略使得字符串長度減小時重新分配內存的概率大大減小。

存取二進制數據:SDS 可以,C 字符串不可以。因爲 C 字符串以空字符作爲字符串結束的標識,而對於一些二進制文件(如圖片等)。內容可能包括空字符串,因此 C 字符串無法正確存取;而 SDS 以字符串長度 len 來作爲字符串結束標識,因此沒有這個問題。此外,由於 SDS 中的 buf 仍然使用了 C 字符串(即以’\0’結尾),因此 SDS 可以使用 C 字符串庫中的部分函數。但是需要注意的是,只有當 SDS 用來存儲文本數據時纔可以這樣使用,在存儲二進制數據時則不行(’\0’不一定是結尾)。

使用:所有的key;數據裏的字符串;AOF緩衝區和用戶輸入緩衝。


鏈表定義

typedef struct listNode {
    //前置節點
    struct listNode *prev;
    //後置節點
    struct listNode *next;
    //節點的值
    void *value;
}listNode

typedef struct list {
    //表頭節點
    listNode.head;
    //表尾節點
    listNode.tail;
    //鏈表所包含的節點數量
    unsigned long len;
    //節點值複製函數
    void *(*dup)(void *ptr);
    //節點值釋放函數
    void *(*free)(void *ptr);
    //節點值對比函數
    int (*match)(void *ptr,void *key);
} list;

Redis鏈表優勢:

①、雙向:鏈表具有前置節點和後置節點的引用,獲取這兩個節點時間複雜度都爲O(1)。與傳統鏈表(單鏈表)相比,Redis鏈表結構的優勢有:普通鏈表(單鏈表):節點類保留下一節點的引用。鏈表類只保留頭節點的引用,只能從頭節點插入刪除

②、無環:表頭節點的 prev 指針和表尾節點的 next 指針都指向 NULL,對鏈表的訪問都是以 NULL結束。

③、帶鏈表長度計數器:通過 len 屬性獲取鏈表長度的時間複雜度爲 O(1)。

④、多態:鏈表節點使用 void* 指針來保存節點值,可以保存各種不同類型的值。

 

鏈表和列表區別

linkedlist和arraylist

鏈表在Redis中的應用非常廣泛,列表(List)的底層實現之一就是雙向鏈表。此外發布與訂閱、慢查詢、監視器等功能也用到了鏈表。


字典
字典又稱爲符號表或者關聯數組、或映射(map),是一種用於保存鍵值對的抽象數據結構。字典中的每一個鍵 key 都是唯一的,通過 key 可以對值來進行查找或修改。Redis 的字典使用哈希表作爲底層實現。哈希(作爲一種數據結構),不僅是 Redis 對外提供的 5 種對象類型的一種(hash),也是 Redis 作爲 Key-Value 數據庫所使用的數據結構。
typedef struct dictht{
//哈希表數組
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩碼,用於計算索引值
//總是等於 size-1
unsigned long sizemask;
//該哈希表已有節點的數量
unsigned long used;
}dictht

/*哈希表是由數組 table 組成,table 中每個元素都是指向 dict.h/dictEntry 結構,
dictEntry 結構定義如下:
*/
typedef struct dictEntry{
//鍵
void *key;
//值
union{
void *val;
uint64_tu64;
int64_ts64;
}v;
//指向下一個哈希表節點,形成鏈表
struct dictEntry *next;
}dictEntry

typedef struct zskiplistNode {
//層
struct zskiplistLevel{
//前進指針
struct zskiplistNode *forward;
//跨度
unsigned int span;
}level[];
//後退指針
struct zskiplistNode *backward;
//分值
double score;
//成員對象
robj *obj;
} zskiplistNode
--鏈表
typedef struct zskiplist{
//表頭節點和表尾節點
structz skiplistNode *header, *tail;
//表中節點的數量
unsigned long length;
//表中層數最大的節點的層數
int level;
}zskiplist;

整數集合

整數集合(intset)是集合(set)的底層實現之一,當一個集合(set)只包含整數值元素,並且這個集合的元素不多時,Redis就會使用整數集合(intset)作爲該集合的底層實現。整數集合(intset)是

Redis用於保存整數值的集合抽象數據類型,它可以保存類型爲int16_t、int32_t 或者int64_t 的整數值,並且保證集合中不會出現重複元素。

typedef struct intset{
//編碼方式
uint32_t encoding;
//集合包含的元素數量
uint32_t length;
//保存元素的數組
int8_t contents[];
}intset;

壓縮列表

壓縮列表(ziplist)是列表鍵和哈希鍵的底層實現之一。當一個列表只包含少量列表項時,並且每個列表項時小整數值或短字符串,那麼Redis會使用壓縮列表來做該列表的底層實現。

壓縮列表(ziplist)是Redis爲了節省內存而開發的,是由一系列特殊編碼的連續內存塊組成的順序型數據結構,一個壓縮列表可以包含任意多個節點(entry),每個節點可以保存一個字節數組或者一個整數值。放到一個連續內存區。

previous_entry_ength: 記錄壓縮列表前一個字節的長度。

encoding:節點的encoding保存的是節點的content的內容類型

content:content區域用於保存節點的內容,節點內容類型和長度由encoding決定。


對象

前面我們講了Redis的數據結構,Redis不是用這些數據結構直接實現Redis的鍵值對數據庫,而是基於這些數據結構創建了一個對象系統。包含字符串對象,列表對象,哈希對象,集合對象和有序集合對象。根據對象的類型可以判斷一個對象是否可以執行給定的命令,也可針對不同的使用場景,對象設置有多種不同的數據結構實現,從而優化對象在不同場景下的使用效率。Redis中的每個對象都是由如下結構表示(列出了與保存數據有關的三個屬性)

typedef struct redisObject {
unsigned type:4;//類型 五種對象類型
unsigned encoding:4;//編碼
void *ptr;//指向底層實現數據結構的指針
//...
int refcount;//引用計數
//...
unsigned lru:22;//記錄最後一次被命令程序訪問的時間
//...
}robj;

type  

type 字段表示對象的類型,佔 4 個比特;目前包括 REDIS_STRING(字符串)、REDIS_LIST (列表)、REDIS_HASH(哈希)、REDIS_SET(集合)、REDIS_ZSET(有序集合)。當我們執行 type 命令時,便是通過讀取 RedisObject 的 type 字段獲得對象的類型,如下所示:

127.0.0.1:6379> type a1 string

encoding

encoding 表示對象的內部編碼,佔 4 個比特。對於 Redis 支持的每種類型,都有至少兩種內部編碼,例如對於字符串,有 int、embstr、raw 三種編碼。通過 encoding 屬性,Redis 可以根據不同的使用場景來爲對象設置不同的編碼,大大提高了 Redis 的靈活性和效率。

以列表對象爲例,有壓縮列表和雙端鏈表兩種編碼方式;

如果列表中的元素較少,Redis 傾向於使用壓縮列表進行存儲,因爲壓縮列表佔用內存更少,而且比雙端鏈表可以更快載入。

當列表對象元素較多時,壓縮列表就會轉化爲更適合存儲大量元素的雙端鏈表。

通過 object encoding 命令,可以查看對象採用的編碼方式,如下所示:

127.0.0.1:6379> object encoding a1 "int"

lru

lru 記錄的是對象最後一次被命令程序訪問的時間,佔據的比特數不同的版本有所不同(如 4.0 版本佔24 比特,2.6 版本佔 22 比特)。

通過對比 lru 時間與當前時間,可以計算某個對象的空轉時間;object idletime 命令可以顯示該空轉時間(單位是秒)。object idletime 命令的一個特殊之處在於它不改變對象的 lru 值。

lru 值除了通過 object idletime 命令打印之外,還與 Redis 的內存回收有關係。如果 Redis 打開了 maxmemory 選項,且內存回收算法選擇的是 volatile-lru 或 allkeys—lru,那麼當Redis 內存佔用超過 maxmemory 指定的值時,Redis 會優先選擇空轉時間最長的對象進行釋放。

refcount

refcount 與共享對象:refcount 記錄的是該對象被引用的次數,類型爲整型。refcount 的作用,主要在於對象的引用計數和內存回收。

當創建新對象時,refcount 初始化爲 1;當有新程序使用該對象時,refcount 加 1;當對象不再被一個新程序使用時,refcount 減 1;當 refcount 變爲 0 時,對象佔用的內存會被釋放。

Redis 中被多次使用的對象(refcount>1),稱爲共享對象。Redis 爲了節省內存,當有一些對象重複出現時,新的程序不會創建新的對象,而是仍然使用原來的對象。這個被重複使用的對象,就是共享對象。目前共享對象僅支持整數值的字符串對象。共享對象的引用次數可以通過 object refcount 命令查看,如下所示。命令執行的結果頁佐證了只有0~9999 之間的整數會作爲共享對象。

127.0.0.1:6379> object refcount a1 (integer) 2147483647

ptr

ptr 指針指向具體的數據,比如:set hello world,ptr 指向包含字符串 world 的 SDS。

 

綜上所述,RedisObject 的結構與對象類型、編碼、內存回收、共享對象都有關係。

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