1、簡介和應用java
Redis是一個由ANSI C語言編寫,性能優秀、支持網絡、可持久化的K-K內存數據庫,並提供多種語言的API。它經常使用的類型主要是 String、List、Hash、Set、ZSet 這5種。redis
String:緩存、限流、計數器、分佈式鎖、分佈式Session算法
Hash:存儲用戶信息、用戶主頁訪問量、組合查詢數據庫
List:微博關注人時間軸列表、簡單隊列數組
Set:贊、踩、標籤、好友關係緩存
Zset:排行榜安全
再好比電商在大促銷時,會用一些特殊的設計來保證系統穩定,扣減庫存能夠考慮以下設計:網絡
經過上面的應用場景能夠看出Redis是很是高效和穩定的,那Redis底層是如何實現的呢?數據結構
2、Redis的對象redisObject併發
當咱們執行set hello world命令時,會有如下數據模型:
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」對應的編碼:
字符串對象的底層實現能夠是int、raw、embstr(上面的表對應有名稱介紹)。embstr編碼是經過調用一次內存分配函數來分配一塊連續的空間,而raw須要調用兩次。
簡單動態字符串(SDS),這種結構更像C++的String或者Java的ArrayList,長度動態可變:
struct sdshdr {
// buf 中已佔用空間的長度
int len;
// buf 中剩餘可用空間的長度
int free;
// 數據空間
char buf[]; // ’\0’空字符結尾
};
複製代碼
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中的列表支持兩端插入和彈出,並能夠得到指定位置(或範圍)的元素,能夠充當數組、隊列、棧等。
typedef struct listNode {
// 前置節點
struct listNode *prev;
// 後置節點
struct listNode *next;
// 節點的值
void *value;
} listNode;
typedef struct list {
// 表頭節點
listNode *head;
// 表尾節點
listNode *tail;
// 節點值複製函數
void *(*dup)(void *ptr);
// 節點值釋放函數
void (*free)(void *ptr);
// 節點值對比函數
int (*match)(void *ptr, void *key);
// 鏈表所包含的節點數量
unsigned long len;
} 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,有興趣能夠閱讀一下源碼。
與雙端鏈表相比,壓縮列表能夠節省內存空間,可是進行修改或增刪操做時,複雜度較高;所以當節點數量較少時,可使用壓縮列表;可是節點數量多時,仍是使用雙端鏈表划算。
4.2 ziplist(壓縮列表)
當一個列表鍵只包含少許列表項,且是小整數值或長度比較短的字符串時,那麼redis就使用ziplist(壓縮列表)來作列表鍵的底層實現。
5、Hash
Hash對象的底層實現能夠是ziplist(壓縮列表)或者hashtable(字典或者也叫哈希表)。
hashtable哈希表能夠實現O(1)複雜度的讀寫操做,所以效率很高。源碼以下:
typedef struct dict {
// 類型特定函數
dictType *type;
// 私有數據
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引
// 當 rehash 不在進行時,值爲 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
// 目前正在運行的安全迭代器的數量
int iterators; /* number of iterators currently running */
} dict;
typedef struct dictht {
// 哈希表數組
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩碼,用於計算索引值
// 老是等於 size - 1
unsigned long sizemask;
// 該哈希表已有節點的數量
unsigned long used;
} dictht;
typedef struct dictEntry {
void *key;
union {void *val;uint64_t u64;int64_t s64;} v;
// 指向下個哈希表節點,造成鏈表
struct dictEntry *next;
} dictEntry;
typedef struct dictType {
// 計算哈希值的函數
unsigned int (*hashFunction)(const void *key);
// 複製鍵的函數
void *(*keyDup)(void *privdata, const void *key);
// 複製值的函數
void *(*valDup)(void *privdata, const void *obj);
// 對比鍵的函數
int (*keyCompare)(void *privdata, const void *key1, const void *key2);
// 銷燬鍵的函數
void (*keyDestructor)(void *privdata, void *key);
// 銷燬值的函數
void (*valDestructor)(void *privdata, void *obj);
} dictType;
複製代碼
上面源碼能夠簡化成以下結構:
Redis中的字典使用hashtable做爲底層實現的話,每一個字典會帶有兩個哈希表,一個平時使用,另外一個僅在rehash(從新散列)時使用。隨着對哈希表的操做,鍵會逐漸增多或減小。爲了讓哈希表的負載因子維持在一個合理範圍內,Redis會對哈希表的大小進行擴展或收縮(rehash),也就是將ht【0】裏面全部的鍵值對分屢次、漸進式的rehash到ht【1】裏。
6、Set
Set集合對象的底層實現能夠是intset(整數集合)或者hashtable(字典或者也叫哈希表)。
typedef struct intset {
// 編碼方式
uint32_t encoding;
// 集合包含的元素數量
uint32_t length;
// 保存元素的數組
int8_t contents[];
} 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(跳躍表)。
typedef struct zskiplist {
// 表頭節點和表尾節點
struct zskiplistNode *header, *tail;
// 表中節點的數量
unsigned long length;
// 表中層數最大的節點的層數
int level;
} zskiplist;
typedef struct zskiplistNode {
// 成員對象
robj *obj;
// 分值
double score;
// 後退指針
struct zskiplistNode *backward;
// 層
struct zskiplistLevel {
// 前進指針
struct zskiplistNode *forward;
// 跨度---前進指針所指向節點與當前節點的距離
unsigned int span;
} level[];
} zskiplistNode;
複製代碼
zadd---zslinsert---平均O(logN), 最壞O(N) zrem---zsldelete---平均O(logN), 最壞O(N) zrank--zslGetRank---平均O(logN), 最壞O(N)