1.內部編碼:
每種數據類型都有不同的編碼,在滿足一定條件下會進行編碼轉換
2.各種編碼對應的數據結構簡單介紹:
A.簡單動態字符串(sds,simple dynamic string):
因傳統C的字符串不能高效支持長度計算與append操作,故redis採用sds替換C默認的字符串表示
傳統C字符串計算長度:strlen(s)複雜度爲O(n)
傳統C字符串n次append:進行n次內存分配(realloc)
sds的實現:
typedef struct sdshdr{
int len;//buf已用長度
int free;//buf剩餘可用長度
char buf[];//實際保存字符串數據的地方
}
因此,sds字符串計算長度複雜度爲O(1);append時候,當free‘小於append字符串長度時進行擴容,減少內存分配(分配機制後面介紹)
B.linkedList雙端鏈表:
typedef struct listNode{ typedef struct list{
struct listNode * prev;//前驅結點 listNode *head;//表頭指針
struct listNode *next;//後繼節點 listNode *tail;//表尾指針
void *value;//節點值 unsigned long len;//節點數量
}listNode; void (*dup)(void *ptr);//複製函數
void (*free)(void *ptr);//釋放函數
int (*match)(void *ptr,void *key);//對比函數
}list;
性能特徵:listNode有prev與next兩個指針,可從兩個方向進行迭代;list有head何tail兩個指針,表頭與表尾插入複雜度爲O(1),即lpush、lpop、rpoplpush等命令高效的原因;list帶len屬性,計算鏈表長度的複雜度爲O(1),因此len命令不會成爲性能瓶頸
C.字典(k-v映射):
redis字典用途:實現redis數據庫鍵空間、hash的底層實現
字典的實現:
typedef struct dict {
dictType *type;// 特定於類型的處理函數
void *privdata;// 類型處理函數的私有數據
dictht ht[2];// 哈希表(2個,第1個是主hash表,第2個用於rehash)
int rehashidx;// 記錄rehash進度的標誌,值爲-1 表示rehash 未進行
int iterators;// 當前正在運作的安全迭代器數量
} dict;
哈希表的實現:
typedef struct dictht {
dictEntry **table;// 哈希表節點指針數組(俗稱桶,bucket)
unsigned long size;// 指針數組的大小
unsigned long sizemask;// 指針數組的長度掩碼,用於計算索引值
unsigned long used;// 哈希表現有的節點數量
} dictht; 其中,table屬性爲數組,數組元素是指向dictEntry結構的指針
dictEntry哈希表節點的實現:
typedef struct dictEntry {
void *key;// 鍵
union { void *val; uint64_t u64; int64_t s64; } v;// 值
struct dictEntry *next;// 鏈往後繼節點
} dictEntry;
next屬性指向下一個dictEntry,多個dictEntry通過next連接成鏈表,當多個鍵擁有相同的hash值時,哈希表用鏈地址法(與jdk1.7的HashMap處理方式一樣)來處理鍵衝突,使用鏈表將這些鍵連接起來
因此,整個字典的結構如下:
D.skipList跳躍表:
跳躍表在redis中唯一作用是實現有序集合zset,通過在每個節點中維持多個指向其他節點的指針,從而達到快速訪問的目的,增、刪、查等操作可以在對數期望時間下完成,結構圖如下:
跳躍表構成:
表頭:負責維護跳躍表的節點指針
表尾:全部由null組成,表示跳躍表的末尾
表節點:保存元素值,及多個層
層:保存指向其他元素的指針,高層指針越過的元素數量總是大於等於底層的指針
跳躍表的實現:
typedef struct zskiplist {
struct zskiplistNode *header, *tail;// 頭節點,尾節點
unsigned long length;// 節點數量
int level;// 目前表內節點的最大層數
} zskiplist;
跳躍表節點的實現:
typedef struct zskiplistNode {
robj *obj;// member 對象
double score;// 分值,允許重複,score相等時還要比較member對象
struct zskiplistNode *backward;// 後退指針,zrevrange等逆序命令用
struct zskiplistLevel {
struct zskiplistNode *forward;// 前進指針
unsigned int span;// 這個層跨越的節點數量
} level[];// 層
} zskiplistNode;
E.整數集合intset:
用於有序、無重複地保存多個整數值,會根據元素值自動選擇合適長度的整數類型來保存,是集合類型set的底層實現之一,當set只保存整數元素且數量不多時會用intset
intset的實現:
typedef struct intset {
uintXX_t encoding;// 保存元素所使用的類型的長度
uintXX_t length;// 元素個數
intXX_t contents[];// 保存元素的數組,元素不重複且從小到大排序
} intset; (PS:XX可以是16、32、64,添加元素、分配內存由encoding決定)
在添加元素時,若intset當前編碼不適用新元素編碼,會觸發對intset的升級,升級不會改變元素的值,且編碼方式由元素中長度最大的那個決定,不可逆,只能由較短編碼升級到較長編碼,升級會引起intset內存重分配,並移動集合在所有元素,複雜度爲O(n),應儘量保持整數範圍一致,儘量避免因個別大整數觸發升級操作,浪費內存
intset添加元素的執行流程如下:
F.zipList壓縮列表:
ziplist 是由一系列特殊編碼的內存塊構成的列表,可保存字符數組或整數值,是hash、zset、list底層實現之一 分佈結構如下圖:
zlbytes:整個ziplist列表佔用內存數,用於內存重分配、計算末端
zltail:達到ziplist表尾節點的偏移量,可不遍歷整個ziplist便彈出尾節點
zllen:ziplist列表的節點數量,該值<65535時表示節點數量,等於時需遍歷
entryX:ziplist所保存的節點,節點長度視內容而定
zlend:用於標誌ziplist的末端,1字節
zipList節點分佈結構:
pre_entry_length:上一個節點長度,從而可通過指針計算跳轉到上一個節點
encoding:content部分所保存的數據類型,00、01、10表示保存的是字符數組,11表示保存的是整數
length:content所保存數據的長度
content:保存節點內容,長度與類型由encoding與length決定
使用zipList好處就是可以最大程度上節省內存,適合存儲小對象與有限長度數據(512字節以內),但列表長度不可無限制,否則對該列表的操作時間會大大增加,得不償失
G.redis對象的數據結構:
由於redis的鍵值可以保存不同類型的值且每種數據類型又對應多種編碼,因此需要對鍵值類型進行檢查及"多態"處理,比如:lpush只能用於列表鍵,del可用於任何鍵,必須爲不同類型的鍵設置不同的處理方式,因此redis構建了自己的類型系統
redis類型系統的主要功能如下:
a.redisObject的實現:
typedef struct redisObject {
unsigned type:4;// 類型
unsigned notused:2;// 對齊位
unsigned encoding:4;// 編碼方式 unsigned
lru:22;// LRU 時間
int refcount;// 引用計數
void *ptr;// 指向對象的值
} robj;
主要屬性:
type:對象所保存的值的類型,0-String,1-list,2-set,3-zset,4-hash
encoding:對象所保存的值的編碼,0-raw,1-int,2-hashtable,3-zipmap,4-linkedlist,5-ziplist,6-intset,7-skiplist
ptr:指向實際保存值的數據結構,由type與encoding共同決定
lru:記錄對象最後一次被訪問的時間,用於輔助lru算法刪除數據
refcount:當前對象被引用的次數,爲0可安全回收
b.基於redisObject對象的類型檢查:
當執行一個處理數據類型的命令時,redis執行以下步驟:
1.根據給定key,在數據庫字典中查找和它像對應的redisObject,如果沒找到,就返回NULL;
2.檢查redisObject的type屬性和執行命令所需的類型是否相符,如果不相符,返回類型錯誤;
3.根據redisObject 的encoding 屬性所指定的編碼,選擇合適的操作函數來處理底層的數據結構;
4.返回數據結構的操作結果作爲命令的返回值
以lpop命令爲例,執行步驟如下圖:
c.共享對象:
redis內部維護一個0到9999的對象池,可通過redis_shared_integers參數配置,當其他類型(如:list、set、zset、hash)的輸入值在該範圍內,則該對象值的指針將指向共享對象(PS:共享對象只能被字典和雙端鏈表這類能帶有指針的數據結構使用,像整數集合和壓縮列表這些只能保存字符串、整數等字面值的內存數據結構,就不能使用共享對象) 對象共享意味着多個引用共享同一個redisObject,這時lru字段也會被共享,導致無法獲取每個對象的最後訪問時間),因此共享對象池與maxmemory+lru策略衝突,使用需注意
d.對redisObject的分配、共享和銷燬機制:
1.每個redisObject 結構都帶有一個refcount 屬性,指示這個對象被引用了多少次;
2.當新創建一個對象時,它的refcount 屬性被設置爲1 ;
3.當對一個對象進行共享時,Redis 將這個對象的refcount 增一;
4.當使用完一個對象之後,或者取消對共享對象的引用之後,程序將對象的refcount 減一;
5.當對象的refcount 降至0 時,這個redisObject 結構,以及它所引用的數據結構的內存,都會被釋放
3.使用何種編碼:
A.對於String類型:
int:8個字節的長整型
embstr:小於等於39個字節的字符串
raw:大於39個字節的字符串
embstr與raw的區別:embstr在創建String對象時,會分配一次空間,包含redisObject與sds,而raw會分配2次空間,分別分配給redisoObject與sds
B.對於hash類型:
當hash類型元素小於hash-max-ziplist-entries配置(默認512個)、且所有值都小於hash-max-ziplist-value配置(默認64字節)時,Redis會使用ziplist作爲哈希的內部實現;
當hash類型無法滿足ziplist條件時,會使用hashtable編碼
C.對於list類型:
當列表的元素個數小於list-max-ziplist-entries配置(默認512個),且列表中每個元素的值都小於list-max-ziplist-value配置時(默認64字節),Redis會選用ziplist來作爲列表的內部實現;
當列表類型無法滿足ziplist條件時會使用linkedlist作爲內部實現
D.對於set類型:
當集合中的元素都是整數且元素個數小於set-maxintset-entries配置(默認512個)時,Redis會選用intset來作爲集合的內部實現;
當集合類型無法滿足intset的條件時,Redis會使用hashtable作爲集合的內部實現
F.對於zset類型:
當有序集合的元素個數小於zset-max-ziplistentries配置(默認128個)且每個元素的值都小於zset-max-ziplist-value配置(默認64字節)時,Redis會用ziplist來作爲有序集合的內部實現;
當ziplist條件不滿足時,有序集合會使用skiplist作爲內部實現
可使用object encoding key 查看指定key當前的編碼