上一篇文章介紹了redis基本的資料結構和物件《redis設計與實現》1-資料結構與物件篇
本文主要關於:
- redis資料庫實現的介紹
- 前面介紹的各種資料,在redis伺服器中的記憶體模型是什麼樣的的。
- RDB檔案將這些記憶體資料持久化後的格式是什麼樣的
- RDB和AOF序列化的區別是什麼
- redis提供什麼機制保障AOF檔案不會一直增長
- RDB檔案轉儲成json檔案和記憶體分析工具介紹
- 客戶端和服務端資料結構介紹
資料庫
伺服器的資料庫
- redis是記憶體型資料庫,所有資料都放在記憶體中
- 儲存這些資料的是redisServer這個結構體,原始碼中該結構體包括大概300多行的程式碼。具體參考server.h/redisServer
- 和資料庫相關的兩個屬性是:
- int型別的dbnum:表示資料庫數量,預設16個
- redisDb指標型別的db:資料庫物件陣列
資料庫物件
所在檔案為server.h。資料庫中所有針對鍵值對的增刪改查,都是對dict做操作
typedef struct redisDb {
dict *dict; /* The keyspace for this DB */
dict *expires; /* Timeout of keys with a timeout set */
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/
dict *ready_keys; /* Blocked keys that received a PUSH */
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
int id; /* Database ID */
long long avg_ttl; /* Average TTL, just for stats */
} redisDb;
複製程式碼
- dict:儲存了該資料庫中所有的鍵值對,鍵都是字串,值可以是多種型別
- expires:儲存了該資料中所有設定了過期時間的key
- blocking_keys:儲存了客戶端阻塞的
- watched_keys:儲存被watch的命令
- id:儲存資料庫索引
- avg_ttl
客戶端切換資料庫
- 客戶端通過select dbnum 命令切換選中的資料庫
- 客戶端的資訊儲存在client這個資料結構中,參考server.h/client
- client的型別為redisDb的db指標指向目前所選擇的資料庫
讀寫鍵空間時的其他操作
讀寫鍵空間時,是針對dict做操作,但是除了完成基本的增改查詢操作,還會執行一些額外的維護操作,包括:
- 讀寫鍵時,會根據是否命中,更新hit和miss次數。
相關命令:info stats keyspace_hits, info stats keyspace_misses
- 讀取鍵後,會更新鍵的LRU時間,前面章節介紹過該欄位
- 讀取時,如果發現鍵已經過期,會先刪除該鍵,然後才執行其他操作
- 如果watch監視了某個鍵,修改時會標記該鍵為髒(dirty)
- 每修改一個鍵,會對髒鍵計數器加1,觸發持久化和複製操作
- 如果開啟通知功能,修改鍵會下發通知
設定過期時間
- expire key ttl:設定生存時間為ttl秒
- pexpire key ttl:設定生存時間為ttl毫秒
- expireat key timestamp:設定過期時間為timstamp的秒數時間戳
- pexpireat key timestamp:過期時間為毫秒時間戳
- persist key:解除過期時間
- ttl key:獲取剩餘生存時間
儲存過期時間
過期時間儲存在expires的字典中,值為long型別的毫秒時間戳
過期鍵刪除策略
各種刪除策略的對比
策略型別 | 描述 | 優點 | 缺點 | redis是否採用 |
---|---|---|---|---|
定時刪除 | 通過定時器實現 | 保證過期鍵能儘快釋放 | 對cpu不友好,影響相應時間和吞吐量 | 否 |
惰性刪除 | 放任不管,查詢時才去檢查 | 對cpu友好 | 沒有被訪問的永遠不會被釋放,相當於記憶體洩露 | 是 |
定期刪除 | 每隔一段時間檢查 | 綜合前面的優點 | 難於確定執行時長和頻率 | 是 |
redis使用的過期鍵刪除策略
redis採用了惰性刪除和定期刪除策略
惰性刪除的實現
- 由db.c中的expireIfNeeded實現
- 每次執行redis命令前都會呼叫該函式對輸入鍵做檢查
定期刪除的實現
- server.c中的serverCron函式執行定時任務
- 函式每次執行時,都從一定數量的資料庫中取出一定數量的鍵進行檢查,並刪除過期鍵
資料庫通知
- 鍵空間通知:客戶端獲取資料庫中鍵執行了什麼命令。實現程式碼為notify.c檔案的notifyKeyspaceEvent函式
subscribe __keyspace@0__:keyname 複製程式碼
- 鍵事件通知:某個命令被什麼鍵執行了
subscribe __keyevent@0__:del 複製程式碼
RDB持久化
- redis是記憶體資料庫,為了避免伺服器程式異常導致資料丟失,redis提供了RDB持久化功能
- 持久化後的RDB檔案是一個經過壓縮的二進位制檔案
RDB檔案的建立與載入
生成rdb檔案的兩個命令如下,實現函式為rdb.c檔案的rdbSave函式:
- SAVE:阻塞redis伺服器程式,知道RDB建立完成。阻塞期間不能處理其他請求
- BGSAVE:派生出子程式,子程式負責建立RDB檔案,父程式繼續處理請求
RDB檔案的載入是在伺服器啟動時自動執行的,實現函式為rdb.c檔案的rdbload函式。載入期間伺服器一直處於阻塞狀態
自動間隔儲存
redis允許使用者通過設定伺服器配置的server選項,讓伺服器每隔一段時間(100ms)自動執行BGSAVE命令(serverCron函式)
//server.c中main函式內部建立定時器,serverCron為定時任務回撥函式
if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
serverPanic("Can't create event loop timers.");
exit(1);
}
複製程式碼
配置引數
// 任意一個配置滿足即執行
save 900 1 // 900s內,對伺服器進行至少1次修改
save 300 10 // 300s內,對伺服器至少修改10次
複製程式碼
資料結構
// 伺服器全域性變數,前面介紹過
struct redisServer {
...
/* RDB persistence */
// 上一次執行save或bgsave後,對資料庫進行了多少次修改
long long dirty; /* Changes to DB from the last save */
long long dirty_before_bgsave; /* Used to restore dirty on failed BGSAVE */
pid_t rdb_child_pid; /* PID of RDB saving child */
struct saveparam *saveparams; /* Save points array for RDB */
int saveparamslen; /* Number of saving points */
char *rdb_filename; /* Name of RDB file */
int rdb_compression; /* Use compression in RDB? */
int rdb_checksum; /* Use RDB checksum? */
// 上一次成功執行save或bgsave的時間
time_t lastsave; /* Unix time of last successful save */
time_t lastbgsave_try; /* Unix time of last attempted bgsave */
time_t rdb_save_time_last; /* Time used by last RDB save run. */
time_t rdb_save_time_start; /* Current RDB save start time. */
int rdb_bgsave_scheduled; /* BGSAVE when possible if true. */
int rdb_child_type; /* Type of save by active child. */
int lastbgsave_status; /* C_OK or C_ERR */
int stop_writes_on_bgsave_err; /* Don't allow writes if can't BGSAVE */
int rdb_pipe_write_result_to_parent; /* RDB pipes used to return the state */
int rdb_pipe_read_result_from_child; /* of each slave in diskless SYNC. */
...
};
// 具體每一個引數對應的變數
struct saveparam {
time_t seconds;
int changes;
};
複製程式碼
RDB檔案結構
概覽
- 頭五個字元為‘redis’常量,標識這個rdb檔案是redis檔案
- dv_version:4位元組,標識了rdb檔案的版本號
- databases:資料庫檔案內容
- EOF:常量,1位元組,標識檔案正文結束
- check_sum:8位元組無符號整形,儲存校驗和,判定檔案是否有損壞
dababases部分
每個database的內容:- SELECTDB:常量,1位元組。標識了後面的位元組為資料庫號碼
- db_number:資料庫號碼
- key_value_pairs:資料庫的鍵值對,如果有過期時間,也放在一起。
key_value_pairs部分
不帶過期時間的鍵值對
type為value的型別,1位元組,代表物件型別或底層編碼,根據type決定如何讀取value
帶過期時間的鍵值對
- EXPIRETIME:常量,1位元組,表示接下來要讀入的是一個以毫秒為單位的過期時間
- ms:8位元組長的無符號整形,過期時間
value的編碼
每個value儲存一個值物件,與type對應。type不同,value的結構,長度也有所不同
字串物件
- type為REDIS_RDB_TYPE_STRING, value為字串物件,而字串物件本身又包含物件的編碼和內容
- 如果編碼為整數型別,編碼後面直接儲存整數值
- 如果編碼為字串型別,分為壓縮和不壓縮
- 如果字串長度<=20位元組,不壓縮
- 如果字串長度>20位元組,壓縮儲存
- REDIS_RDB_ENC_LZF:常量,標識字串被lzf演算法壓縮過
- compressed_len:被壓縮後的長度
- origin_len:字串原始長度
- compressed_string:壓縮後的內容
列表物件
- type為REDIS_RDB_TYPE_LIST, value為列表物件
- list_length:記錄列表的長度
- item:以字串物件來處理
集合物件
- typw為REDIS_RDB_TYPE_SET,value為集合物件
- set_size: 集合大小
- elem:以字串物件來處理
雜湊物件
- type為REDIS_RDB_TYPE_HASH, value為雜湊物件
- hash_size:雜湊物件大小
- key-value都以字串物件處理
有序集合物件
- type為REDIS_RDB_TYPE_ZSET,value為有序集合物件
intset編碼集合
- type為REDIS_RDB_TYPE_SET_INTSET, value為整數集合物件
- 先將結合轉換為字串物件,然後儲存。讀入時,將字串物件轉為整數集合物件
ziplist編碼的物件(包括列表,雜湊,有序集合)
- type為REDIS_RDB_TYPE_LIST_ZIPLIST, REDIS_RDB_TYPE_HASH_ZIPLIST, REDIS_RDB_TYPE_ZSET_ZIPLIST
- 先將壓縮列表轉換為字串物件,儲存到rdb檔案
- 讀取時根據type型別,讀入字串,轉換為壓縮列表物件
分析RDB檔案
使用linux自帶的od命令
使用linux自帶的od命令可以檢視rdb檔案資訊,比如od -c dump.rdb,以Ascii列印,下圖顯示docker建立的redis中,空的rdb檔案輸出的內容
工具
AOF持久化
AOF寫入與同步
除了RDB持久化外,redis還提供了AOF持久化功能。區別如下:
- RDB通過儲存資料庫中鍵值對記錄資料庫狀態
- AOF通過儲存伺服器執行的寫命令來記錄資料庫狀態
AOF持久化分為三步:
- 命令追加:命令append到redisServer全域性變數的aof_buf成員中
- 檔案寫入:
- 檔案同步
事件結束時呼叫flushAppendOnlyFile函式,考慮是否將aof_buf內容寫到AOF檔案裡(引數決定)
- always:所有內容寫入並同步到AOF檔案(寫入的是緩衝區,同步時從緩衝區刷到磁碟)
- everysec:預設值。寫入AOF檔案,如果上次同步時間距現在草稿1s,同步AOF。
- no:只寫入AOF檔案,由系統決定何時同步
AOF載入與還原
伺服器只需要讀入並執行一遍AOF命令即可還原資料庫狀態,讀取的步驟如下:
- 建立一個不帶網路連線的偽客戶端:因為命令只能在客戶端執行
- 從AOF讀取一條寫命令
- 使用客戶端執行該命令
- 重複上面的步驟,直到完成
AOF重寫
- 隨著時間流逝,AOF檔案內容會越來越大,影響redis效能。redis提供重寫功能解決該問題。
- 重寫是通過讀取redis當前資料狀態完成的,而不是解析AOF檔案
- 為了不影響redis正常響應,重寫功能通過建立子程式(注意不是執行緒)完成
- 為了解決父子程式資料不一致問題(父程式接收新的請求),redis設定了AOF重寫緩衝區。新的命令在AOF緩衝區和AOF重寫緩衝區中雙寫。
事件
redis是一個事件驅動程式,事件包括兩大類:
- 檔案事件:socket通訊完成一系列操作
- 時間事件:某些需要在給定時間執行的操作
檔案事件
- redis基於Reactor模式開發事件處理器,使用IO多路複用監聽套接字。關於IO多路複用可參考之前的文章五種io模型對比
- ,雖然事件處理器以單執行緒執行,通過io多路複用,能同時監聽多個套接字實現高效能
事件處理器的構成
- 檔案事件:套接字操作的抽象
- io多路複用程式:同時監聽多個套接字,並向事件分派器傳送事件。多個套接字按佇列排序
- 檔案事件分派器:接收套接字,根據事件型別呼叫相應的事件處理器
- 事件處理器:不同的函式實現不同的事件
IO多路複用的實現
可選的io多路複用包括select,epoll,evport,kqueue實現。每種實現都放在單獨的檔案中。編譯時根據不同的巨集切換不同的實現
事件型別
#define AE_NONE 0 /* No events registered. */
#define AE_READABLE 1 /* Fire when descriptor is readable. */
#define AE_WRITABLE 2 /* Fire when descriptor is writable. */
複製程式碼
處理器
redis為檔案事件編寫了多個處理器,分別用於實現不同的網路需求,在networking.c檔案中,包括:
- 連線應答處理器:監聽套接字,接收客戶端命令請求。對應函式為acceptTcpHandler。內部呼叫socket程式設計的accpt函式
- 命令請求處理器:負責讀入套接字中的命令請求內容。對應函式為readQueryFromClient。內部呼叫socket程式設計的read函式
- 命令回覆處理器:負責將回復通過套接字返回給客戶。對應函式為sendReplyToClient。內部呼叫socket班車的write函式
時間事件
分類
時間事件分類以下兩大類,取決於時間處理器的返回值:
- 定時事件:返回AE_NOMORE(-1)
- 週期性事件:非AE_NOMORE值。單機版只有serverCron一個週期性事件
屬性
時間事件包括三個屬性:
- id:伺服器建立的全域性唯一標識
- when:事件到達時間
- timeProc:處理器,一個函式
實現
- 所有時間事件放在一個無序連結串列中
- 執行時需要遍歷連結串列
- ae.c/aeCreateTimeEvent:建立時間處理器
- aeSearchNearestTimer:返回距離當前時間最近的時間事件
- ae.c/processTimeEvents:遍歷時間處理器並執行
事件排程
- 事件排程和執行由ae.c/aeProcessEvents函式負責
- 該函式被放在ae.c/aeMain函式中的一段迴圈裡面,不斷執行直到伺服器關閉
- aeMain被server.c的main函式呼叫
int main() {
...
aeMain(server.el);
...
}
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
}
}
複製程式碼
客戶端
redis伺服器為每個連線的客戶端建立了一個redisClient的結構,儲存客戶端狀態資訊。所有客戶端的資訊放在一個連結串列裡。可通過client list命令檢視
struct redisServer {
...
list *clients;
...
}
複製程式碼
客戶端資料結構如下:
typedef struct client {
uint64_t id; /* Client incremental unique ID. */
//客戶端套接字描述符,偽客戶端該值為-1(包括AOF還原和執行Lua指令碼的命令)
int fd; /* Client socket. */
redisDb *db; /* Pointer to currently SELECTed DB. */
// 客戶端名字,預設為空,可通過client setname設定
robj *name; /* As set by CLIENT SETNAME. */
// 輸入緩衝區,儲存客戶端傳送的命令請求,不能超過1G
sds querybuf; /* Buffer we use to accumulate client queries. */
size_t qb_pos; /* The position we have read in querybuf. */
sds pending_querybuf; /* If this client is flagged as master, this buffer
represents the yet not applied portion of the
replication stream that we are receiving from
the master. */
size_t querybuf_peak; /* Recent (100ms or more) peak of querybuf size. */
// 解析querybuf,得到引數個數
int argc; /* Num of arguments of current command. */
// 解析querybuf,得到引數值
robj **argv; /* Arguments of current command. */
// 根據前面的argv[0], 找到這個命令對應的處理函式
struct redisCommand *cmd, *lastcmd; /* Last command executed. */
int reqtype; /* Request protocol type: PROTO_REQ_* */
int multibulklen; /* Number of multi bulk arguments left to read. */
long bulklen; /* Length of bulk argument in multi bulk request. */
// 伺服器返回給客戶端的可被空間,固定buff用完時才會使用
list *reply; /* List of reply objects to send to the client. */
unsigned long long reply_bytes; /* Tot bytes of objects in reply list. */
size_t sentlen; /* Amount of bytes already sent in the current
buffer or object being sent. */
// 客戶端的建立時間
time_t ctime; /* Client creation time. */
// 客戶端與伺服器最後一次互動的時間
time_t lastinteraction; /* Time of the last interaction, used for timeout */
// 客戶端空轉時間
time_t obuf_soft_limit_reached_time;
// 客戶端角色和狀態:REDIS_MASTER, REDIS_SLAVE, REDIS_LUA_CLIENT等
int flags; /* Client flags: CLIENT_* macros. */
// 客戶端是否通過身份驗證的標識
int authenticated; /* When requirepass is non-NULL. */
int replstate; /* Replication state if this is a slave. */
int repl_put_online_on_ack; /* Install slave write handler on ACK. */
int repldbfd; /* Replication DB file descriptor. */
off_t repldboff; /* Replication DB file offset. */
off_t repldbsize; /* Replication DB file size. */
sds replpreamble; /* Replication DB preamble. */
long long read_reploff; /* Read replication offset if this is a master. */
long long reploff; /* Applied replication offset if this is a master. */
long long repl_ack_off; /* Replication ack offset, if this is a slave. */
long long repl_ack_time;/* Replication ack time, if this is a slave. */
long long psync_initial_offset; /* FULLRESYNC reply offset other slaves
copying this slave output buffer
should use. */
char replid[CONFIG_RUN_ID_SIZE+1]; /* Master replication ID (if master). */
int slave_listening_port; /* As configured with: SLAVECONF listening-port */
char slave_ip[NET_IP_STR_LEN]; /* Optionally given by REPLCONF ip-address */
int slave_capa; /* Slave capabilities: SLAVE_CAPA_* bitwise OR. */
multiState mstate; /* MULTI/EXEC state */
int btype; /* Type of blocking op if CLIENT_BLOCKED. */
blockingState bpop; /* blocking state */
long long woff; /* Last write global replication offset. */
list *watched_keys; /* Keys WATCHED for MULTI/EXEC CAS */
dict *pubsub_channels; /* channels a client is interested in (SUBSCRIBE) */
list *pubsub_patterns; /* patterns a client is interested in (SUBSCRIBE) */
sds peerid; /* Cached peer ID. */
listNode *client_list_node; /* list node in client list */
/* Response buffer */
// 記錄buf陣列目前使用的位元組數
int bufpos;
// (16*1024)=16k,伺服器返回給客戶端的內容緩衝區。固定大小,儲存一下固定返回值(如‘ok’)
char buf[PROTO_REPLY_CHUNK_BYTES];
} client;
複製程式碼
伺服器
伺服器記錄了redis伺服器所有的資訊,包括前面介紹的一些,羅列主要的如下:
struct redisServer {
...
// 所有資料資訊
redisDb *db;
// 所有客戶端資訊
list *clients;
/* time cache */
// 系統當前unix時間戳,秒
time_t unixtime; /* Unix time sampled every cron cycle. */
time_t timezone; /* Cached timezone. As set by tzset(). */
int daylight_active; /* Currently in daylight saving time. */
// 系統當前unix時間戳,毫秒
long long mstime; /* Like 'unixtime' but with milliseconds resolution. */
// 預設沒10s更新一次的時鐘快取,用於計算鍵idle時長
unsigned int lruclock; /* Clock for LRU eviction */
// 抽樣相關的引數
struct {
// 上次抽樣時間
long long last_sample_time; /* Timestamp of last sample in ms */
// 上次抽樣時,伺服器已經執行的命令數
long long last_sample_count;/* Count in last sample */
// 抽樣結果
long long samples[STATS_METRIC_SAMPLES];
int idx;
} inst_metric[STATS_METRIC_COUNT];
// 記憶體峰值
size_t stat_peak_memory; /* Max used memory record */
// 關閉伺服器的標識
int shutdown_asap; /* SHUTDOWN needed ASAP */
// bgsave命令子程式的id
pid_t rdb_child_pid; /* PID of RDB saving child */
// bgrewriteaof子程式id
pid_t aof_child_pid; /* PID if rewriting process */
// serverCron執行次數
int cronloops; /* Number of times the cron function run */
...
}
複製程式碼
參考
- 《redis設計與實現》
- rdbtool wiki
- rdr分析工具