1、與其他數據庫的對比
最近系統中引入了Redis,在應用中發現Reids具有關係型數據庫或其他緩存服務器所不具備的優點。
與關係型數據庫如Mysql相比,Reids屬於非關係型數據庫,類似於Nosql,不同數據之間不需要有關聯關係。
memcache也可以用來存儲鍵值映射,同是對內存操作,和Redis性能差別不大,但是Redis具備以兩種形式將數據寫入硬盤的能力,並且除了存儲普通的字符串鍵值外,還可以存儲其他四種數據結構,而memcache只能存儲字符串鍵。所以Redis可以用來解決更廣泛的問題,而且不只可以用來當做緩存數據庫,更可以用作主數據庫使用。
2、Redis的五種數據類型
Redis可以存儲五種鍵和數據類型間的映射,分別是STRING、LIST、SET、HASH、ZSET,其中STRING、LIST、SET、HASH在大多數編程語言中都存在,在實現和作用上也比較相似。另外ZSET是特有的一種數據結構,是一種有序集合。
以下是五種數據類型之間的對比和特點。
結構類型 | 結構存儲的值 | 結構操作 |
---|---|---|
STRING | 字符串、整數、浮點數 | 可以對字符串或字符串一部分執行操作,對整數和浮點數進行++ –操作 |
LIST | 鏈表,鏈表的每個節點包含一個字符串 | 可以在鏈表兩端進行push pull(起到了棧的作用),根據偏移量進行trim,讀取單個或多個元素,根據值查找或刪除元素 |
SET | 無序集合 | 添加/獲取/移除元素,判斷是否存在,交差並集合,隨機獲取元素 |
HASH | 無序散列表 | 添加、移除、獲取鍵值對,獲取所有鍵值對 |
ZSET | 有序集合,由字符串和分數組成,元素排列順序根據分值 | 添加、獲取、刪除元素,根據分值獲取元素 |
2.1 STRING
Redis的字符串映射的鍵和值都是字符串類型,可以存儲字符串、整數或浮點數。和memcache的鍵值對作用相同。除了普通字符串類型,在存儲一些複雜結構時,比如本次在系統中存儲一個java對象,就是將對象序列化爲字符串進行儲存,另外,也可以將對象序列化爲json或xml進行存儲。
一般STRING類型有以下應用場景:
- 緩存,存儲基礎數據信息(文章、用戶、記錄等)。僅當update/insert時訪問數據庫並更新緩存,其他時候只訪問緩存,可以顯著減輕數據庫壓力。
- 用作session服務器,一般服務器集羣中共享session會存儲在數據庫、session服務器、redis中,存取較快。
- 計數器等操作頻繁的數據請求,因爲STRING類型可以存儲整數或浮點數並可以自增,所以用作計數器、pingback等非常方便。
2.2 LIST
LIST就是我們經常用到的數據結構中的隊列,Redis的LIST是雙端隊列,兩端都可以進行Push和pull操作,所以也可以當做棧來使用,另外還可以獲取一定範圍內的列表。
LIST有以下應用場景:
- 分佈式系統中服務間調用可以使用LIST實現消息隊列,實現異步調用,使應用間解耦。
- 高併發場景下可以使用LIST實現限流。
- 文章分頁或者是有序的列表數據可以使用LIST,因爲可以在某個範圍內存取元素。
2.3 HASH
上文中提到了STRING可以用來存儲序列化的對象,但是如果這個對象需要進行操作的話,需要先解析再修改再序列化保存,無法直接修改。而HASH類型可以解決這個問題,在Redis中,HASH結構可以解決這個問題,它的鍵也是一個鍵值對,用戶可以直接修改HASH結構中某個屬性的值。所以適合存儲對象。
HASH有以下應用場景:
- 存儲用戶數據,用戶基礎數據是請求頻率較高的,但是屬性較多且經常需要修改,所以可以存儲在HASH,修改時update某個屬性就可以。
2.4 SET
SET是數據結構中的集合,與列表不同的是它不保存重複數據,並且元素無序,所以不能獲取一定範圍內的元素,也不能根據索引獲取。Redis支持對集合進行交併差集計算,在很多場景下都可以發揮作用。
SET有以下應用場景:
- 數據排重,比如記錄今天訪問的用戶,可以使用集合。
- 標籤操作,系統中有個功能是打標籤,每打一個標籤,可以將該數據保存在以標籤id爲key的set中,展示時直接展示該set。
- 用戶關係操作,比如計算用戶的共同好友等。
2.5 ZSET
有序集合是一種特殊的集合,它既具備集合不重複元素的特點,又可以進行排序。但它和list不同的是,它不是根據下標排序,所以排序不固定,而是它的value中包含一個分值,分值可以是分數、訪問量等等,根據自定義類別進行排序。
ZSET有以下應用場景:
- 排行榜,排序操作最常用的應用就是排行榜,比如考試分數、訪問量、點贊量。考慮的一個應用場景是在wiki中的精選文章中,對文章進行排序。因爲精選文章相對固定但需要實時變動,應用ZSET比較合適。
3、底層數據結構的實現
我們知道Redis是使用ANSI C實現的,那它是怎麼實現的集合等複雜數據結構,比較令人好奇,在閱讀了《Redis設計與實現》這本書後,得到了比較準確的答案。首先我們需要了解一下Redis中使用到的數據結構。
3.1 簡單動態字符串
最開始以爲Redis是使用c++中的char數組來存儲字符串類型,其實並不是這樣,它定義了一個名爲simple dynamic string(SDS)的結構體用來保存字符串類型,結構如下:
struct sdshdr{
int len;
int free;
int buf[];
}
其中:
- len:buf中已用長度
- free:bug剩餘可用長度
- buf[] : 字符數組
我們存儲的字符串就是存儲在buf數組中,這樣做的原因是char[]並不能滿足Redis的操作需求,或是會帶來較大的性能消耗,比如append,獲取長度等等。像一些高級語言,也普遍有這種實現方式,像之前在閱讀PHP數據結構源碼時,string類型也是由數組和一個標誌長度的int值實現。這樣獲取長度的複雜度就是O(1)。另外還有一些好處:
- 防止緩衝區溢出
字符串長度增加時,如果內存相鄰地址已有內容,則會發生緩衝區溢出的現象,而Redis在擴展字符串時,會先檢查free長度,如果不夠時,會先拓展空間。 - 減少內存分配次數
SDS的擴展策略是小於1MB時,每次擴展到之前的二倍大小,大於1MB時,每次擴展1MB,所以不需要每次變化都重新分配內存。另外由於釋放空間時採用惰性空間釋放,減少了內存分配次數。 - 惰性空間釋放
SDS釋放空間時並不真正釋放內存空間,而是修改free的值,既能避免內存泄露,又減少內存分配次數。 - 二進制安全
c字符串末尾默認是空字符,所以在首次讀入空字符時會被認爲字符串結束。而SDS記錄了len長度,可以通過長度獲取內容,有空字符也不影響,所以可以用來存儲二進制數據。
簡單動態字符串是Redis最重要的數據結構,鍵值等字符串類型是使用它,另外還有AOF模塊中的AOF緩衝區,以及客戶端狀態中的輸入緩衝區也是由它實現。
3.2 鏈表
Redis中的LIST實現之一就是鏈表的。由鏈表和節點兩種數據結構組成,節點用來存儲數據和指針,鏈表結構封裝了一些複製和刪除節點的操作。
下面是節點的結構體:
typedef struct listNode{
struct listNode *prev;
struct listNode *next;
void *value;
}listNode;
普通鏈表節點一般只有一個指針指向下一個節點,而這裏面有pre和next兩個指針,實現了一個雙向鏈表。
下面是鏈表結構體:
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;
list結構體中除了包含head頭結點,tail尾節點,len長度外,還封裝了複製節點、刪除節點、匹配節點的方法。
3.3 hashtable
Redis中HASH的實現方式之一是hashtable,由dict和dictht兩種數據結構實現。
其中字典dict的數據結構:
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
int iterators; /* number of iterators currently running */
} dict;
type 屬性 和privdata 屬性是針對不同類型的鍵值對,爲創建多態字典而設置的。ht 屬性是一個包含兩個項(兩個哈希表)的數組。指向了兩個哈希表dictht。
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
其中dictht中的table是用來存儲kv元素的,每個dictEntry包含一對kv。
最後,哈希表節點dictEntry結構定義爲:
typeof struct dictEntry{
void *key;
union{
void *val;
uint64_tu64;
int64_ts64;
}
struct dictEntry *next;
}
其中的next指針是爲了解決hash衝突時,使用了鏈地址法組成鏈表。在此不對如何解決hash衝突和使用的散列算法進行深入討論。
如圖所示:
圖片來自引用
HASH的另一種實現方式是ziplist。
3.4 intset
Redis中SET的實現方式有兩種,其中一種是hashtable,在上文中已經有過了解。另外一種是intset,這是一個整數集合,裏面存的爲某種同一類型的整數,支持如下三種長度的整數:
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))
intset結構體爲:
typedef struct intset{
//編碼方式
uint32_t enconding;
//集合包含的元素數量
uint32_t length;
//保存元素的數組
int8_t contents[];
}intset;
其中裏面的數據按照從大到小的元素排列。並且存儲的類型由encoding決定。在集合中查找元素的複雜度爲O(logN)。但插入時設計到從16位升級到32位或64位,所以複雜度不一定。並且升級後不能再降級。
3.5 skiplist
Redis的ZSET實現方式之一是跳躍表,另一種是ziplist。
跳躍表(skiplist)是一種有序數據結構,它通過在每個節點中維持多個指向其他節點的指針,從而達到快速訪問節點的目的。跳躍表是一種隨機化的數據,跳躍表以有序的方式在層次化的鏈表中保存元素,效率和平衡樹媲美 ——查找、刪除、添加等操作都可以在對數期望時間下完成,並且比起平衡樹來說,跳躍表的實現要簡單直觀得多。
跳躍表主要由鏈表和節點組成,下面是鏈表zskiplist的數據結構。
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
//最大節點成熟
int level;
} zskiplist;
可以看到,和普通鏈表不同的是,多了一個level用來記錄表中層數最大的節點的層數。
下面是節點的結構體:
typedef struct zskiplistNode {
// member
robj *obj;
// 分值
double score;
// 後退指針
struct zskiplistNode *backward;
// 層
struct zskiplistLevel {
// 前進指針
struct zskiplistNode *forward;
// 這個層跨越的節點數量
unsigned int span;
} level[];
} zskiplistNode;
其中除了value和分數score外,還包含了一個level[]數組,這就是每個節點裏的分層指針數組。
如圖所示:
圖片來自引用
其中每個節點的層數都是1到32的隨機數,節點間是按照分值大小來進行排序。
3.6 ziplist
這是一種壓縮列表,在LIST和HASH中使用,因爲它保存在連續的內存空間中,所以比價節省內存空間,但在插入時每次都需要重新分配空間。
如圖所示:
圖片來自引用
其中,包含如下屬性:
- zlbytes:用於記錄整個壓縮列表佔用的內存字節數
- zltail:記錄要列表尾節點距離壓縮列表的起始地址有多少字節
- zllen:記錄了壓縮列表包含的節點數量。
- entryX:要說列表包含的各個節點
- zlend:用於標記壓縮列表的末端
3.7 Redis對象的實現
上面講解了Reids實現中使用到的數據結構,其中Redis中的每個對象都不是由固定的數據結構實現,而是會根據數據類型大小選擇不同的實現方式,以下爲對應表。
Redis對象 | 實現方式 |
---|---|
STRING | int 實現 |
embstr編碼的簡單動態字符串(SDS)實現 | |
SDS實現 | |
LIST | ziplist壓縮列表實現 |
鏈表實現 | |
HASH | ziplist實現 |
字典hashtable實現 | |
SET | intset整數集合實現 |
hashtable實現 | |
ZSET | ziplist實現 |
跳錶skiplist、hashtable實現 |
總結
通過閱讀《Redis設計與實現》前兩章,對Redis的底層實現有了初步的瞭解。後續會繼續研究Redis兩種持久化方式和線程架構方面。
參考來源:
1. 《Redis設計與實現》
2. 《Redis實戰》
3. http://www.cnblogs.com/jaycekon/p/6227442.html
4. https://blog.csdn.net/u011531613/article/details/70193720?locationNum=7&fps=1
5. https://blog.csdn.net/wcf373722432/article/details/78678504
6. https://www.cnblogs.com/ysocean/p/9080942.html