我的核心模塊如圖 1-10。

圖 1-10
Client 客戶端,官方提供了 C 語言開發(fā)的客戶端,可以發(fā)送命令,性能分析和測(cè)試等。
網(wǎng)絡(luò)層事件驅(qū)動(dòng)模型,基于 I/O 多路復(fù)用,封裝了一個(gè)短小精悍的高性能 ae 庫,全稱是 a simple event-driven programming library。
在 ae 這個(gè)庫里面,我通過 aeApiState 結(jié)構(gòu)體對(duì) epoll、select、kqueue、evport四種 I/O 多路復(fù)用的實(shí)現(xiàn)進(jìn)行適配,讓上層調(diào)用方感知不到在不同操作系統(tǒng)實(shí)現(xiàn) I/O 多路復(fù)用的差異。
Redis 中的事件可以分兩大類:一類是網(wǎng)絡(luò)連接、讀、寫事件;另一類是時(shí)間事件,也就是特定時(shí)間觸發(fā)的事件,比如定時(shí)執(zhí)行 rehash 操作。
命令解析和執(zhí)行層,負(fù)責(zé)執(zhí)行客戶端的各種命令,比如 SET、DEL、GET等。
內(nèi)存分配和回收,為數(shù)據(jù)分配內(nèi)存,提供不同的數(shù)據(jù)結(jié)構(gòu)保存數(shù)據(jù)。
持久化層,提供了 RDB 內(nèi)存快照文件 和 AOF 兩種持久化策略,實(shí)現(xiàn)數(shù)據(jù)可靠性。
高可用模塊,提供了副本、哨兵、集群實(shí)現(xiàn)高可用。
監(jiān)控與統(tǒng)計(jì),提供了一些監(jiān)控工具和性能分析工具,比如監(jiān)控內(nèi)存使用、基準(zhǔn)測(cè)試、內(nèi)存碎片、bigkey 統(tǒng)計(jì)、慢指令查詢等。
掌握了整體架構(gòu)和模塊后,接下來進(jìn)入 src 源碼目錄,使用如下指令執(zhí)行 redis-server可執(zhí)行程序啟動(dòng) Redis。
./redis-server ../redis.conf
每個(gè)被啟動(dòng)的服務(wù)我都會(huì)抽象成一個(gè) redisServer,源碼定在server.h 的redisServer 結(jié)構(gòu)體。
這個(gè)結(jié)構(gòu)體包含了存儲(chǔ)鍵值對(duì)的數(shù)據(jù)庫實(shí)例、redis.conf 文件路徑、命令列表、加載的 Modules、網(wǎng)絡(luò)監(jiān)聽、客戶端列表、RDB AOF 加載信息、配置信息、RDB 持久化、主從復(fù)制、客戶端緩存、數(shù)據(jù)結(jié)構(gòu)壓縮、pub/sub、Cluster、哨兵等一些列 Redis 實(shí)例運(yùn)行的必要信息。
結(jié)構(gòu)體字段很多,不再一一列舉,部分核心字段如下。
truct redisServer {
pid_t pid; /* 主進(jìn)程 pid. */
pthread_t main_thread_id; /* 主線程 id */
char *configfile; /*redis.conf 文件絕對(duì)路徑*/
redisDb *db; /* 存儲(chǔ)鍵值對(duì)數(shù)據(jù)的 redisDb 實(shí)例 */
int dbnum; /* DB 個(gè)數(shù) */
dict *commands; /* 當(dāng)前實(shí)例能處理的命令表,key 是命令名,value 是執(zhí)行命令的入口 */
aeEventLoop *el;/* 事件循環(huán)處理 */
int sentinel_mode; /* true 則表示作為哨兵實(shí)例啟動(dòng) */
/* 網(wǎng)絡(luò)相關(guān) */
int port;/* TCP 監(jiān)聽端口 */
list *clients; /* 連接當(dāng)前實(shí)例的客戶端列表 */
list *clients_to_close; /* 待關(guān)閉的客戶端列表 */
client *current_client; /* 當(dāng)前執(zhí)行命令的客戶端*/
};
1.2.1 數(shù)據(jù)存儲(chǔ)原理
其中redisDb *db指針非常重要,它指向了一個(gè)長度為 dbnum(默認(rèn) 16)的 redisDb 數(shù)組,它是整個(gè)存儲(chǔ)的核心,我就是用這玩意來存儲(chǔ)鍵值對(duì)。
redisDb
redisDb 結(jié)構(gòu)體定義如下。
typedef struct redisDb {
dict *dict;
dict *expires;
dict *blocking_keys;
dict *ready_keys;
dict *watched_keys;
int id;
long long avg_ttl;
unsigned long expires_cursor;
list *defrag_later;
clusterSlotToKeyMapping *slots_to_keys;
} redisDb;
dict 和 expires
- dict 和 expires 是最重要的兩個(gè)屬性,底層數(shù)據(jù)結(jié)構(gòu)是字典,分別用于存儲(chǔ)鍵值對(duì)數(shù)據(jù)和 key 的過期時(shí)間。
- expires,底層數(shù)據(jù)結(jié)構(gòu)是 dict 字典,存儲(chǔ)每個(gè) key 的過期時(shí)間。
?
MySQL:“為什么分開存儲(chǔ)?”
好問題,之所以分開存儲(chǔ),是因?yàn)檫^期時(shí)間并不是每個(gè) key 都會(huì)設(shè)置,它不是鍵值對(duì)的固有屬性,分開后雖然需要兩次查找,但是能節(jié)省內(nèi)存開銷。
blocking_keys 和 ready_keys
底層數(shù)據(jù)結(jié)構(gòu)是 dict 字典,主要是用于實(shí)現(xiàn) BLPOP 等阻塞命令。
當(dāng)客戶端使用 BLPOP 命令阻塞等待取出列表元素的時(shí)候,我會(huì)把 key 寫到 blocking_keys 中,value 是被阻塞的客戶端。
當(dāng)下一次收到 PUSH 命令執(zhí)時(shí),我會(huì)先檢查 blocking_keys 中是否存在阻塞等待的 key,如果存在就把 key 放到 ready_keys 中,在下一次 Redis 事件處理過程中,會(huì)遍歷 ready_keys 數(shù)據(jù),并從 blocking_keys 中取出阻塞的客戶端響應(yīng)。
watched_keys
用于實(shí)現(xiàn) watch 命令,存儲(chǔ) watch 命令的 key。
id
Redis 數(shù)據(jù)庫的唯一 ID,一個(gè) Redis 服務(wù)支持多個(gè)數(shù)據(jù)庫,默認(rèn) 16 個(gè)。
avg_ttl
用于統(tǒng)計(jì)平均過期時(shí)間。
expires_cursor
統(tǒng)計(jì)過期事件循環(huán)執(zhí)行的次數(shù)。
defrag_later
保存逐一進(jìn)行碎片整理的 key 列表。
slots_to_keys
僅用于 Cluster 模式,當(dāng)使用 Cluster 模式的時(shí)候,只能有一個(gè)數(shù)據(jù)庫 db 0。slots_to_keys 用于記錄 cluster 模式下,存儲(chǔ) key 與哈希槽映射關(guān)系的數(shù)組。
dict
Redis 使用 dict 結(jié)構(gòu)來保存所有的鍵值對(duì)(key-value)數(shù)據(jù),這是一個(gè)散列表,所以 key 查詢時(shí)間復(fù)雜度是 O(1) 。
所謂散列表,我們可以類比 Java 中的 HashMap,其實(shí)就是一個(gè)數(shù)組,數(shù)組的每個(gè)元素叫做哈希桶。
dict 結(jié)構(gòu)體源碼在 dict.h 中定義。
struct dict {
dictType *type;
dictEntry **ht_table[2];
unsigned long ht_used[2];
long rehashidx;
int16_t pauserehash;
signed char ht_size_exp[2];
};
dict 的結(jié)構(gòu)體里,有 dictType type,*ht_table[2],long rehashidx 三個(gè)很重要的結(jié)構(gòu)。
- type 存儲(chǔ)了 hash 函數(shù),key 和 value 的復(fù)制等函數(shù);
- ht_table[2],長度為 2 的數(shù)組,默認(rèn)使用 ht_table[0] 存儲(chǔ)鍵值對(duì)數(shù)據(jù)。我會(huì)使用 ht_table[1] 來配合實(shí)現(xiàn)漸進(jìn)式 reahsh 操作。
- rehashidx 是一個(gè)整數(shù)值,用于標(biāo)記是否正在執(zhí)行 rehash 操作,-1 表示沒有進(jìn)行 rehash。如果正在執(zhí)行 rehash,那么其值表示當(dāng)前 rehash 操作執(zhí)行的 ht_table[1] 中的 dictEntry 數(shù)組的索引。
- pauserehash 表示 rehash 的狀態(tài),大于 0 時(shí)表示 rehash 暫停了,小于 0 表示出錯(cuò)了。
- ht_used[2],長度為 2 的數(shù)組,表示每個(gè)哈希桶存儲(chǔ)了多少個(gè) 鍵值對(duì)實(shí)體(dictEntry),值越大,哈希沖突的概率越高。
- ht_size_exp[2],每個(gè)散列表的大小,也就是哈希桶個(gè)數(shù)。
重點(diǎn)關(guān)注 ht_table 數(shù)組,數(shù)組每個(gè)位置叫做哈希桶,就是這玩意保存了所有鍵值對(duì),每個(gè)哈希桶的類型是 dictEntry。
?
MySQL:“Redis 支持那么多的數(shù)據(jù)類型,哈希桶咋保存?”
他的玄機(jī)就在 dictEntry 中,每個(gè) dict 有兩個(gè) ht_table,用于存儲(chǔ)鍵值對(duì)數(shù)據(jù)和實(shí)現(xiàn)漸進(jìn)式 rehash。
dictEntry 結(jié)構(gòu)如下。
typedef struct dictEntry {
void *key;
union {
// 指向?qū)嶋H value 的指針
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
// 散列表沖突生成的鏈表
struct dictEntry *next;
void *metadata[];
} dictEntry;
- *key 指向鍵值對(duì)的鍵的指針,指向一個(gè) sds 對(duì)象,key 都是 string 類型。
- v 是鍵值對(duì)的 value 值,是個(gè) union(聯(lián)合體),當(dāng)它的值是 uint64_t、int64_t 或 double 數(shù)字類型時(shí),就不再需要額外的存儲(chǔ),這有利于減少內(nèi)存碎片。(為了節(jié)省內(nèi)存操碎了心)當(dāng)值為非數(shù)字類型,就是用 val 指針存儲(chǔ)。
- next指向另一個(gè) dictEntry 結(jié)構(gòu), 多個(gè) dictEntry 可以通過 next 指針串連成鏈表, 從這里可以看出, ht_table 使用鏈地址法來處理鍵碰撞:當(dāng)多個(gè)不同的鍵擁有相同的哈希值時(shí),哈希表用一個(gè)鏈表將這些鍵連接起來。*
哈希桶并沒有保存值本身,而是指向具體值的指針,從而實(shí)現(xiàn)了哈希桶能存不同數(shù)據(jù)類型的需求。
redisObject
dictEntry 的 *val 指針指向的值實(shí)際上是一個(gè) redisObject 結(jié)構(gòu)體,這是一個(gè)非常重要的結(jié)構(gòu)體。
我的 key 只能是字符串類型,而 value 可以是 String、Lists、Set、Sorted Set、散列表等數(shù)據(jù)類型。
鍵值對(duì)的值都被包裝成 redisObject 對(duì)象, redisObject 在 server.h 中定義。
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS;
int refcount;
void *ptr;
} robj;
- type:記錄了對(duì)象的類型,string、set、hash 、Lis、Sorted Set 等,根據(jù)該類型來確定是哪種數(shù)據(jù)類型,使用什么樣的 API 操作。
- encoding:編碼方式,表示 ptr 指向的數(shù)據(jù)類型具體數(shù)據(jù)結(jié)構(gòu),即這個(gè)對(duì)象使用了什么數(shù)據(jù)結(jié)構(gòu)作為底層實(shí)現(xiàn)保存數(shù)據(jù)。同一個(gè)對(duì)象使用不同編碼實(shí)現(xiàn)內(nèi)存占用存在明顯差異,內(nèi)部編碼對(duì)內(nèi)存優(yōu)化非常重要。
- lru:LRU_BITS:LRU 策略下對(duì)象最后一次被訪問的時(shí)間,如果是 LFU 策略,那么低 8 位表示訪問頻率,高 16 位表示訪問時(shí)間。
- refcount :表示引用計(jì)數(shù),由于 C 語言并不具備內(nèi)存回收功能,所以 Redis 在自己的對(duì)象系統(tǒng)中添加了這個(gè)屬性,當(dāng)一個(gè)對(duì)象的引用計(jì)數(shù)為 0 時(shí),則表示該對(duì)象已經(jīng)不被任何對(duì)象引用,則可以進(jìn)行垃圾回收了。
- ptr 指針:指向?qū)ο蟮牡讓訉?shí)現(xiàn)數(shù)據(jù)結(jié)構(gòu),指向值的指針。
如圖 1-11 是由 redisDb、dict、dictEntry、redisObejct 關(guān)系圖:

圖1-11
注意,一開始的時(shí)候,我只使用 ht_table[0] 這個(gè)散列表讀寫數(shù)據(jù),ht_table[1] 指向 NULL,當(dāng)這個(gè)散列表容量不足,觸發(fā)擴(kuò)容操作,這時(shí)候就會(huì)創(chuàng)建一個(gè)更大的散列表 ht_table[1]。
接著我會(huì)使用漸進(jìn)式 rehash 的方式將 ht_table[0] 的數(shù)據(jù)遷移到 ht_table[1] 上,全部遷移完成后,再修改下指針,讓 ht_table[0] 指向擴(kuò)容后的散列表,回收掉原來的散列表,ht_table[1] 再次指向 NULL。