一、核心模塊如圖?
- Client 客戶端,官方提供了 C 語言開發的客戶端,可以發送命令,性能分析和測試等。
- 網絡層事件驅動模型,基于 I/O 多路復用,封裝了一個短小精悍的高性能 ae 庫,全稱是
a simple event-driven programming library
。- 在 ae 這個庫里面,我通過
aeApiState
結構體對epoll、select、kqueue、evport
四種 I/O 多路復用的實現進行適配,讓上層調用方感知不到在不同操作系統實現 I/O 多路復用的差異。 - Redis 中的事件可以分兩大類:一類是網絡連接、讀、寫事件;另一類是時間事件,比如定時執行 rehash 、RDB 內存快照生成,過期鍵值對清理操作。
- 在 ae 這個庫里面,我通過
- 命令解析和執行層,負責執行客戶端的各種命令,比如
SET、DEL、GET
等。 - 內存分配和回收,為數據分配內存,提供不同的數據結構保存數據。
- 持久化層,提供了 RDB 內存快照文件 和 AOF 兩種持久化策略,實現數據可靠性。
- 高可用模塊,提供了副本、哨兵、集群實現高可用。
- 監控與統計,提供了一些監控工具和性能分析工具,比如監控內存使用、基準測試、內存碎片、bigkey 統計、慢指令查詢等。
二、數據存儲原理
????????在掌握存儲原理之前,先看一下全局架構圖,后邊慢慢分析他們的作用。
????????如圖 是由 redisDb、dict、dictEntry、redisObejct 關系圖:
1.redisServer
????????每個被啟動的服務我都會抽象成一個 redisServer,源碼定在server.h
的redisServer
結構體。結構體字段很多,不再一一列舉,部分核心字段如下。
truct redisServer {pid_t pid; /* 主進程 pid. */pthread_t main_thread_id; /* 主線程 id */char *configfile; /*redis.conf 文件絕對路徑*/redisDb *db; /* 存儲鍵值對數據的 redisDb 實例 */int dbnum; /* DB 個數 */dict *commands; /* 當前實例能處理的命令表,key 是命令名,value 是執行命令的入口 */aeEventLoop *el;/* 事件循環處理 */int sentinel_mode; /* true 則表示作為哨兵實例啟動 *//* 網絡相關 */int port;/* TCP 監聽端口 */list *clients; /* 連接當前實例的客戶端列表 */list *clients_to_close; /* 待關閉的客戶端列表 */client *current_client; /* 當前執行命令的客戶端*/
};
????????這個結構體包含了存儲鍵值對的數據庫實例、redis.conf 文件路徑、命令列表、加載的 Modules、網絡監聽、客戶端列表、RDB AOF 加載信息、配置信息、RDB 持久化、主從復制、客戶端緩存、數據結構壓縮、pub/sub、Cluster、哨兵等一系列 Redis 實例運行的必要信息。
????????接下來我們分別看下他們之間的關系和作用。
2.redisDb
????????其中redisDb *db
指針非常重要,它指向了一個長度為 dbnum(默認 16)的 redisDb 數組,它是整個存儲的核心,我就是用這玩意來存儲鍵值對。
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 是最重要的兩個屬性,底層數據結構是字典,分別用于存儲鍵值對數據和 key 的過期時間。
- expires,底層數據結構是 dict 字典,存儲每個 key 的過期時間。
3.dict
????????Redis 使用 dict 結構來保存所有的鍵值對(key-value)數據,這是一個散列表,所以 key 查詢時間復雜度是 O(1) 。
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 的結構體里,有 dictType *type
,**ht_table[2]
,long rehashidx
三個很重要的結構。
- type 存儲了 hash 函數,key 和 value 的復制等函數;
- ht_table[2],長度為 2 的數組,默認使用 ht_table[0] 存儲鍵值對數據。我會使用 ht_table[1] 來配合實現漸進式 reahsh 操作。
- rehashidx 是一個整數值,用于標記是否正在執行 rehash 操作,-1 表示沒有進行 rehash。如果正在執行 rehash,那么其值表示當前 rehash 操作執行的 ht_table[1] 中的 dictEntry 數組的索引。
????????重點關注 ht_table 數組,數組每個位置叫做哈希桶,就是這玩意保存了所有鍵值對,每個哈希桶的類型是 dictEntry。
MySQL:“Redis 支持那么多的數據類型,哈希桶咋保存?”
????????他的玄機就在 dictEntry 中,每個 dict 有兩個 ht_table,用于存儲鍵值對數據和實現漸進式 rehash。
typedef struct dictEntry {void *key;union {// 指向實際 value 的指針void *val;uint64_t u64;int64_t s64;double d;} v;// 散列表沖突生成的鏈表struct dictEntry *next;void *metadata[];
} dictEntry;
*key
指向鍵值對的鍵的指針,指向一個 sds 對象,key 都是 string 類型。- v 是鍵值對的 value 值,是個 union(聯合體),當它的值是 uint64_t、int64_t 或 double 數字類型時,就不再需要額外的存儲,這有利于減少內存碎片。(為了節省內存操碎了心)當值為非數字類型,就是用
val
指針存儲。 *next
指向另一個 dictEntry 結構, 多個 dictEntry 可以通過 next 指針串連成鏈表, 從這里可以看出, ht_table 使用鏈地址法來處理鍵碰撞:當多個不同的鍵擁有相同的哈希值時,哈希表用一個鏈表將這些鍵連接起來。
4.redisObject
????????dictEntry
的 *val
指針指向的值實際上是一個 redisObject
結構體,這是一個非常重要的結構體。
????????我的 key 是字符串類型,而 value 可以是 String、Lists、Set、Sorted Set、Hashes 等數據類型。
????????鍵值對的值都被包裝成 redisObject 對象, redisObject
在 server.h
中定義。
typedef struct redisObject {unsigned type:4;unsigned encoding:4;unsigned lru:LRU_BITS;int refcount;void *ptr;
} robj;
- type:記錄了對象的類型,string、set、hash 、Lis、Sorted Set 等,根據該類型來確定是哪種數據類型,這樣我才知道該使用什么指令執行嘛。
- encoding:編碼方式,表示 ptr 指向的數據類型具體數據結構,即這個對象使用了什么數據結構作為底層實現保存數據。同一個對象使用不同編碼內存占用存在明顯差異,節省內存,這玩意功不可沒。
- lru:LRU_BITS:LRU 策略下對象最后一次被訪問的時間,如果是 LFU 策略,那么低 8 位表示訪問頻率,高 16 位表示訪問時間。
- refcount :表示引用計數,由于 C 語言并不具備內存回收功能,所以 Redis 在自己的對象系統中添加了這個屬性,當一個對象的引用計數為 0 時,則表示該對象已經不被任何對象引用,則可以進行垃圾回收了。
- ptr 指針:指向值的指針,對象的底層實現數據結構。