Redis作為基於記憶體的非關係型的K-V資料庫。因讀寫響應快速、原子操作、提供了多種資料型別String、List、Hash、Set、Sorted Set、在專案中有著廣泛的使用,今天我們來探討下下Redis的資料結構是如何實現的。
Redis將資料儲存在redisDb中,預設0~15共16個db。每個庫都是獨立的空間,不必擔心key衝突問題,可通過select命令切換db。叢集模式使用db0
typedef struct redisDb {
dict *dict; /* The keyspace for this DB */
dict *expires; /* Timeout of keys with a timeout set */
...
} redisDb;
2.2.1 雜湊字典dict
K-V儲存我們最先想到的就是map,在Redis中通過dict實現,資料結構如下:
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
unsigned long iterators; /* number of iterators currently running */
} dict;
hash資料存在兩個特點:
針對hash資料的特點,存在hash碰撞的問題,dict通過dictType中的函數能夠解決這個問題
typedef struct dictType {
uint64_t (*hashFunction)(const void *key);
int (*keyCompare)(void *privdata, const void *key1, const void *key2);
...
} dictType;
2.2.2 雜湊表 dictht
dict.h/dictht表示一個雜湊表,具體結構如下:
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
鍵值對dict.h/dictEntry
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
使用hash表就一定會存在hash碰撞的問題,hash碰撞後在當前陣列節點形成一個連結串列,在資料量超過hash表長度的情況下,就會存在大量節點稱為連結串列,極端情況下時間複雜度會從O(1)變為O(n);如果hash表的資料再不斷減少,會造成空間浪費的情況。Redis會針對這兩種情況根據負載因子做擴充套件與收縮操作:
收縮操作:
Redis在擴容時如果全量擴容會因為資料量問題導致使用者端操作短時間內無法處理,所以採用漸進式 rehash進行擴容,步驟如下:
在漸進式 rehash 進行期間,字典的刪除(delete)、查詢(find)、更新(update)等操作會在兩個雜湊表上進行;在字典裡面查詢一個鍵的話, 程式會先在 ht[0] 裡面進行查詢,如果沒找到的話,就會繼續到ht[1]裡面進行查詢;新新增到字典的鍵值對一律會被儲存到 ht[1] 裡面,而ht[0]則不再進行任何新增操作:這一措施保證了ht[0]包含的鍵值對數量會只減不增(如果長時間不進行操作時,事件輪詢進行這種操作),並隨著rehash操作的執行而最終變成空表。
dict.h/redisObject
Typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS;
int refcount;
void *ptr;
}
完整結構圖如下:
String 字串存在有三種型別:字串,整數,浮點。主要有以下使用場景
1)頁面動態快取
比如生成一個動態頁面,首次可以將後臺資料生成頁面,並且儲存到redis字串中。再次存取,不再進行資料庫請求,直接從redis中讀取該頁面。特點是:首次存取比較慢,後續存取快速。
2)資料快取
在前後分離式開發中,有些資料雖然儲存在資料庫,但是更改特別少。比如有個全國地區表。當前端發起請求後,後臺如果每次都從關係型資料庫讀取,會影響網站整體效能。
我們可以在第一次存取的時候,將所有地區資訊儲存到redis字串中,再次請求,直接從資料庫中讀取地區的json字串,返回給前端。
3)資料統計
redis整型可以用來記錄網站存取量,某個檔案的下載量。(原子自增自減)
4)時間內限制請求次數
比如已登入使用者請求簡訊驗證碼,驗證碼在5分鐘內有效的場景。當用戶首次請求了簡訊介面,將使用者id儲存到redis 已經傳送簡訊的字串中,並且設定過期時間為5分鐘。當該使用者再次請求簡訊介面,發現已經存在該使用者傳送簡訊記錄,則不再傳送簡訊。
5)分散式session
當我們用nginx做負載均衡的時候,如果我們每個從伺服器上都各自儲存自己的session,那麼當切換了伺服器後,session資訊會由於不共用而會丟失,我們不得不考慮第三應用來儲存session。通過我們用關係型資料庫或者redis等非關係型資料庫。關係型資料庫儲存和讀取效能遠遠無法跟redis等非關係型資料庫。
Redis並沒有直接使用C字串實現String型別,在Redis3.2版本之前通過SDS實現
Typedef struct sdshdr {
int len;
int free;
char buf[];
};
3.3.1 查詢時間複雜度
C獲取字串長度的複雜度為O(N)。而SDS通過len記錄長度,從C的O(n)變為O(1)。
3.3.2 緩衝區溢位
C字串不記錄自身長度容易造成緩衝區溢位(buffer overflow)。SDS的空間分配策略完全杜絕了發生緩衝區溢位的可能性,當需要對SDS進行修改時,會先檢查SDS的空間是否滿足修改所需的要求,如果不滿足的話SDS的空間擴充套件至執行修改所需的大小,然後才執行實際的修改操作,所以使用SDS既不需要手動修改SDS的空間大小,也不會出現緩衝區溢位問題。
在SDS中,buf陣列的長度不一定就是字元數量加一,陣列裡面可以包含未使用的位元組,而這些位元組的數量就由SDS的free屬性記錄。通過未使用空間,SDS實現了空間預分配和惰性空間釋放兩種優化策略:
3.3.3 二進位制安全
C字串中的字元必須符合某種編碼(比如 ASCII,並且除了字串的末尾之外,字串裡面不能包含空字元, 否則最先被程式讀入的空字元將被誤認為是字串結尾。
SDS的API都是二進位制安全的(binary-safe):都會以處理二進位制的方式來處理SDS存放在buf陣列裡的資料,程式不會對其中的資料做任何限制、過濾、或者假設 —— 資料在寫入時是什麼樣的,它被讀取時就是什麼樣。redis不是用這個陣列來儲存字元,而是用它來儲存一系列二進位制資料。
String型別所儲存的資料可能會幾byte存在大量這種型別資料,但len、free屬性的int型別會佔用4byte共8byte儲存,3.2之後會根據字串大小使用sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64資料結構儲存,具體結構如下:
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
redisObject包裝儲存的value值,通過字元集編碼對資料儲存進行優化,string型別的編碼方式有如下三種:
redis作為k-v資料儲存,因查詢和操作的時間複雜度都是O(1)和豐富的資料型別及資料結構的優化,瞭解了這些資料型別和結構更有利於我們平時對於redis的使用。下一期將對其它常用資料型別List、Hash、Set、Sorted Set所使用的ZipList、QuickList、SkipList做進一步介紹,對於文章中不清晰不準確的地方歡迎大家一起討論交流。
作者:盛旭