Redis 單機服務器實現
1. Redis 服務器
Redis服務器負責與客戶端建立網絡連接,處理髮送的命令請求,在數據庫中保存客戶端執行命令所產生的數據,並且通過一系列資源管理措施來維持服務器自身的正常運轉。本次主要剖析server.c
文件,本文主要介紹Redis服務器的一下幾個實現:
- 命令的執行過程
- Redis服務器的週期性任務
- maxmemory的策略
- Redis服務器的main函數
其他的註釋請上github查看:Redis 單機服務器實現源碼註釋
2. 命令的執行過程
Redis一個命令的完整執行過程如下:
- 客戶端發送命令請求
- 服務器接收命令請求
- 服務器執行命令請求
- 將回復發送給客戶端
關於命令接收與命令回覆,在Redis 網絡連接庫剖析一文已經詳細剖析過,本篇主要針對第三步,也就是服務器執行命令的過程進行剖析。
服務器在接收到命令後,會將命令以對象的形式保存在服務器client
的參數列表robj **argv
中,因此服務器執行命令請求時,服務器已經讀入了一套命令參數保存在參數列表中。執行命令的過程對應的函數是processCommand()
,源碼如下:
// 如果client沒有被關閉則返回C_OK,調用者可以繼續執行其他的操作,否則返回C_ERR,表示client被銷燬
int processCommand(client *c) {
// 如果是 quit 命令,則單獨處理
if (!strcasecmp(c->argv[0]->ptr,"quit")) {
addReply(c,shared.ok);
c->flags |= CLIENT_CLOSE_AFTER_REPLY; //設置client的狀態爲回覆後立即關閉,返回C_ERR
return C_ERR;
}
// 從數據庫的字典中查找該命令
c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);
// 不存在的命令
if (!c->cmd) {
flagTransaction(c); //如果是事務狀態的命令,則設置事務爲失敗
addReplyErrorFormat(c,"unknown command '%s'",
(char*)c->argv[0]->ptr);
return C_OK;
// 參數數量不匹配
} else if ((c->cmd->arity > 0 && c->cmd->arity != c->argc) ||
(c->argc < -c->cmd->arity)) {
flagTransaction(c); //如果是事務狀態的命令,則設置事務爲失敗
addReplyErrorFormat(c,"wrong number of arguments for '%s' command",
c->cmd->name);
return C_OK;
}
/* Check if the user is authenticated */
// 如果服務器設置了密碼,但是沒有認證成功
if (server.requirepass && !c->authenticated && c->cmd->proc != authCommand)
{
flagTransaction(c); //如果是事務狀態的命令,則設置事務爲失敗
addReply(c,shared.noautherr);
return C_OK;
}
// 如果開啓了集羣模式,則執行集羣的重定向操作,下面的兩種情況例外:
/*
1. 命令的發送是主節點服務器
2. 命令沒有key
*/
if (server.cluster_enabled &&
!(c->flags & CLIENT_MASTER) &&
!(c->flags & CLIENT_LUA &&
server.lua_caller->flags & CLIENT_MASTER) &&
!(c->cmd->getkeys_proc == NULL && c->cmd->firstkey == 0 &&
c->cmd->proc != execCommand))
{
int hashslot;
int error_code;
// 從集羣中返回一個能夠執行命令的節點
clusterNode *n = getNodeByQuery(c,c->cmd,c->argv,c->argc,
&hashslot,&error_code);
// 返回的節點不合格
if (n == NULL || n != server.cluster->myself) {
// 如果是執行事務的命令,則取消事務
if (c->cmd->proc == execCommand) {
discardTransaction(c);
} else {
// 將事務狀態設置爲失敗
flagTransaction(c);
}
// 執行client的重定向操作
clusterRedirectClient(c,n,hashslot,error_code);
return C_OK;
}
}
// 如果服務器有最大內存的限制
if (server.maxmemory) {
// 按需釋放一部分內存
int retval = freeMemoryIfNeeded();
// freeMemoryIfNeeded()函數之後需要衝洗從節點的輸出緩衝區,這可能導致被釋放的從節點是一個活躍的client
// 如果當前的client被釋放,返回C_ERR
if (server.current_client == NULL) return C_ERR;
// 如果命令會耗費大量的內存但是釋放內存失敗
if ((c->cmd->flags & CMD_DENYOOM) && retval == C_ERR) {
// 將事務狀態設置爲失敗
flagTransaction(c);
addReply(c, shared.oomerr);
return C_OK;
}
}
// 如果 BGSAVE 命令執行錯誤而且服務器是一個主節點,那麼不接受寫命令
if (((server.stop_writes_on_bgsave_err &&
server.saveparamslen > 0 &&
server.lastbgsave_status == C_ERR) ||
server.aof_last_write_status == C_ERR) &&
server.masterhost == NULL &&
(c->cmd->flags & CMD_WRITE ||
c->cmd->proc == pingCommand))
{
// 將事務狀態設置爲失敗
flagTransaction(c);
// 如果上一次執行AOF成功回覆BGSAVE錯誤回覆
if (server.aof_last_write_status == C_OK)
addReply(c, shared.bgsaveerr);
else
addReplySds(c,
sdscatprintf(sdsempty(),
"-MISCONF Errors writing to the AOF file: %s\r\n",
strerror(server.aof_last_write_errno)));
return C_OK;
}
// 如果沒有足夠的良好的從節點而且用戶配置了 min-slaves-to-write,那麼不接受寫命令
if (server.masterhost == NULL &&
server.repl_min_slaves_to_write &&
server.repl_min_slaves_max_lag &&
c->cmd->flags & CMD_WRITE &&
server.repl_good_slaves_count < server.repl_min_slaves_to_write)
{
// 將事務狀態設置爲失敗
flagTransaction(c);
addReply(c, shared.noreplicaserr);
return C_OK;
}
// 如果這是一個只讀的從節點服務器,則不接受寫命令
if (server.masterhost && server.repl_slave_ro &&
!(c->flags & CLIENT_MASTER) &&
c->cmd->flags & CMD_WRITE)
{
addReply(c, shared.roslaveerr);
return C_OK;
}
// 如果處於發佈訂閱模式,但是執行的不是發佈訂閱命令,返回
if (c->flags & CLIENT_PUBSUB &&
c->cmd->proc != pingCommand &&
c->cmd->proc != subscribeCommand &&
c->cmd->proc != unsubscribeCommand &&
c->cmd->proc != psubscribeCommand &&
c->cmd->proc != punsubscribeCommand) {
addReplyError(c,"only (P)SUBSCRIBE / (P)UNSUBSCRIBE / PING / QUIT allowed in this context");
return C_OK;
}
// 如果是從節點且和主節點斷開了連接,不允許從服務器帶有過期數據,返回
if (server.masterhost && server.repl_state != REPL_STATE_CONNECTED &&
server.repl_serve_stale_data == 0 &&
!(c->cmd->flags & CMD_STALE))
{
flagTransaction(c);
addReply(c, shared.masterdownerr);
return C_OK;
}
// 如果服務器處於載入狀態,如果命令不是CMD_LOADING標識,則不執行,返回
if (server.loading && !(c->cmd->flags & CMD_LOADING)) {
addReply(c, shared.loadingerr);
return C_OK;
}
// 如果lua腳本超時,限制執行一部分命令,如shutdown、scriptCommand
if (server.lua_timedout &&
c->cmd->proc != authCommand &&
c->cmd->proc != replconfCommand &&
!(c->cmd->proc == shutdownCommand &&
c->argc == 2 &&
tolower(((char*)c->argv[1]->ptr)[0]) == 'n') &&
!(c->cmd->proc == scriptCommand &&
c->argc == 2 &&
tolower(((char*)c->argv[1]->ptr)[0]) == 'k'))
{
flagTransaction(c);
addReply(c, shared.slowscripterr);
return C_OK;
}
// 執行命令
// client處於事務環境中,但是執行命令不是exec、discard、multi和watch
if (c->flags & CLIENT_MULTI &&
c->cmd->proc != execCommand && c->cmd->proc != discardCommand &&
c->cmd->proc != multiCommand && c->cmd->proc != watchCommand)
{
// 除了上述的四個命令,其他的命令添加到事務隊列中
queueMultiCommand(c);
addReply(c,shared.queued);
// 執行普通的命令
} else {
call(c,CMD_CALL_FULL);
// 保存寫全局的複製偏移量
c->woff = server.master_repl_offset;
// 如果因爲BLPOP而阻塞的命令已經準備好,則處理client的阻塞狀態
if (listLength(server.ready_keys))
handleClientsBlockedOnLists();
}
return C_OK;
}
我們總結出執行命令的大致過程:
- 查找命令。對應的代碼是:
c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);
- 執行命令前的準備。對應這些判斷語句。
- 執行命令。對應代碼是:
call(c,CMD_CALL_FULL);
我們就大致就這三個過程詳細解釋。
2.1 查找命令
lookupCommand()
函數是對dictFetchValue(server.commands, name);
的封裝。而這個函數的意思是:從server.commands
字典中查找name
命令。這個保存命令表的字典,鍵是命令的名稱,值是命令表的地址。因此我們介紹服務器初始化時的一個操作,就是創建一張命令表。命令表代碼簡化表示如下:
struct redisCommand redisCommandTable[] = {
{"get",getCommand,2,"rF",0,NULL,1,1,1,0,0},
{"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0},
......
};
我們只展示了命令表的兩條,可以通過COMMAND COUNT
命令查看命令的個數。雖然只有兩條,但是可以說明問題。
首先命令表是就是一個數組,數組的每個成員都是一個struct redisCommand
結構體,對每個數組成員都進行了初始化。我們一次對每個值進行分析:以GET
命令爲例子。
- char *name:命令的名字。對應
"get"
。 - redisCommandProc *proc:命令實現的函數。對應
getCommand
。 - int arity:參數個數,-N表示大於等於N。對應
2
。 - char *sflags:命令的屬性,用以下字符作爲標識。對應
"rF"
。
- w:寫入命令,會修改數據庫。
- r:讀取命令,不會修改數據庫。
- m:一旦執行會增加內存使用,如果內存短缺則不被允許執行。
- a:管理員命令,例如:SAVE or SHUTDOWN。
- p:發佈訂閱有關的命令。
- f:強制進行復制的命令,無視服務器的髒鍵。
- s:不能在腳本中執行的命令。
- R:隨機命令。相同的鍵有相同的參數,在相同的數據庫中,可能會有不同的結果。
- S:如果在腳本中調用,那麼會對這個命令的輸出進行一次排序。
- l:當載入數據庫時,允許執行該命令。
- t:從節點服務器持有過期數據時,允許執行的命令。
- M:不能在 MONITOR 下自動傳播的命令。
- k:爲該命令執行一個隱式的 ASKING,所以在集羣模式下,如果槽被標記爲’importing’,那這個命令會被接收。
- F:快速執行的命令。時間複雜度爲O(1) or O(log(N))的命令只要內核調度爲Redis分配時間片,那麼就不應該在執行時被延遲。
- int flags:sflags的二進制標識形式,可以通過位運算進行組合。對應
0
。 - redisGetKeysProc *getkeys_proc:從命令中獲取鍵的參數,是一個可選的功能,一般用於三個字段不夠執行鍵的參數的情況。對應
NULL
。 - int firstkey:第一個參數是 key。對應
1
。 - int lastkey:最後一個參數是 key。對應
1
。 - int keystep:從第一個 key 到最後一個 key 的步長。MSET 的步長是 2 因爲:key,val,key,val,…。對應
1
。 - long long microseconds:記錄執行命令的耗費總時長。對應
0
。 - long long calls:記錄命令被執行的總次數。對應
0
。
當從命令表中找到命令後,會將找到的命令的地址,返回給struct redisCommand *cmd, *lastcmd;
這兩個指針保存起來。到此查找命令的操作就完成。
2.2 執行命令前的準備
此時,命令已經在命令表中查找到,並且保存在了對應的指針中。但是真正執行前,還進行了許多的情況的判斷。我們簡單列舉幾種。
- 首先就是判斷命令的參數是否匹配。
- 檢查服務器的認證是否通過。
- 集羣模式下的判斷。
- 服務器最大內存限制是否通過。
- 某些情況下,不接受寫命令。
- 發佈訂閱模式。
- 是否是lua腳本中的命令。
- 等等……
所以,命令執行的過程還是很複雜的,簡單總結一句:命令不易,何況人生。
2.3 執行命令
執行命令調用了call(c,CMD_CALL_FULL)
函數,該函數是執行命令的核心。但是不用想,這個函數一定是對回調函數c->cmd->proc(c)
的封裝,因爲proc
指向命令的實現函數。我們貼出該函數的代碼:
void call(client *c, int flags) {
long long dirty, start, duration;
int client_old_flags = c->flags; //備份client的flags
// 將命令發送給 MONITOR
if (listLength(server.monitors) &&
!server.loading &&
!(c->cmd->flags & (CMD_SKIP_MONITOR|CMD_ADMIN)))
{
replicationFeedMonitors(c,server.monitors,c->db->id,c->argv,c->argc);
}
// 清除一些需要按照命令需求設置的標誌,以防干擾
c->flags &= ~(CLIENT_FORCE_AOF|CLIENT_FORCE_REPL|CLIENT_PREVENT_PROP);
// 初始化Redis操作數組,用來追加命令的傳播
redisOpArrayInit(&server.also_propagate);
/* Call the command. */
// 備份髒鍵數
dirty = server.dirty;
// 獲取執行命令的開始時間
start = ustime();
// 執行命令
c->cmd->proc(c);
// 命令的執行時間
duration = ustime()-start;
// 命令修改的鍵的個數
dirty = server.dirty-dirty;
if (dirty < 0) dirty = 0;
// 當執行 EVAL 命令時正在加載AOF,而且不希望Lua調用的命令進入slowlog或填充統計信息
if (server.loading && c->flags & CLIENT_LUA)
flags &= ~(CMD_CALL_SLOWLOG | CMD_CALL_STATS); //取消慢查詢和記錄統計信息的標誌
// 如果函數調用者是Lua腳本,且命令的flags或客戶端的flags指定了強制傳播,我們要強制EVAL調用者傳播腳本
if (c->flags & CLIENT_LUA && server.lua_caller) {
// 如果指定了強制將命令傳播到從節點
if (c->flags & CLIENT_FORCE_REPL)
server.lua_caller->flags |= CLIENT_FORCE_REPL; //強制執行lua腳本的client要傳播命令到從節點
// 如果指定了強制將節點傳播到AOF中
if (c->flags & CLIENT_FORCE_AOF)
server.lua_caller->flags |= CLIENT_FORCE_AOF; //強制執行lua腳本的client要傳播命令到AOF文件
}
// 命令的flags指定了慢查詢標誌,要將總的統計信息推入慢查詢日誌中
if (flags & CMD_CALL_SLOWLOG && c->cmd->proc != execCommand) {
char *latency_event = (c->cmd->flags & CMD_FAST) ?
"fast-command" : "command";
// 記錄將延遲事件和延遲時間關聯到延遲診斷的字典中
latencyAddSampleIfNeeded(latency_event,duration/1000);
// 將總的統計信息推入慢查詢日誌中
slowlogPushEntryIfNeeded(c->argv,c->argc,duration);
}
// 命令的flags指定了CMD_CALL_STATS,更新命令的統計信息
if (flags & CMD_CALL_STATS) {
c->lastcmd->microseconds += duration;
c->lastcmd->calls++;
}
// 如果client設置了強制傳播的標誌或修改了數據集,則將命令發送給從節點服務器或追加到AOF中
if (flags & CMD_CALL_PROPAGATE &&
(c->flags & CLIENT_PREVENT_PROP) != CLIENT_PREVENT_PROP)
{
// 保存傳播的標誌,初始化爲空
int propagate_flags = PROPAGATE_NONE;
// 如果命令修改了數據庫中的鍵,則要傳播到AOF和從節點中
if (dirty) propagate_flags |= (PROPAGATE_AOF|PROPAGATE_REPL);
// 如果client設置了強制AOF和複製的標誌,則設置傳播的標誌
if (c->flags & CLIENT_FORCE_REPL) propagate_flags |= PROPAGATE_REPL;
if (c->flags & CLIENT_FORCE_AOF) propagate_flags |= PROPAGATE_AOF;
// 如果client的flags設置了CLIENT_PREVENT_REPL/AOF_PROP,表示阻止命令的傳播到從節點或AOF,則取消傳播對應標誌
if (c->flags & CLIENT_PREVENT_REPL_PROP ||
!(flags & CMD_CALL_PROPAGATE_REPL))
propagate_flags &= ~PROPAGATE_REPL;
if (c->flags & CLIENT_PREVENT_AOF_PROP ||
!(flags & CMD_CALL_PROPAGATE_AOF))
propagate_flags &= ~PROPAGATE_AOF;
// 如果至少設置了一種傳播,則執行相應傳播命令操作
if (propagate_flags != PROPAGATE_NONE)
propagate(c->cmd,c->db->id,c->argv,c->argc,propagate_flags);
}
// 清除一些需要按照命令需求設置的標誌,以防干擾
c->flags &= ~(CLIENT_FORCE_AOF|CLIENT_FORCE_REPL|CLIENT_PREVENT_PROP);
// 恢復client原始的flags
c->flags |= client_old_flags &
(CLIENT_FORCE_AOF|CLIENT_FORCE_REPL|CLIENT_PREVENT_PROP);
// 傳播追加在Redis操作數組中的命令
if (server.also_propagate.numops) {
int j;
redisOp *rop;
// 如果命令的flags設置傳播的標誌
if (flags & CMD_CALL_PROPAGATE) {
// 遍歷所有的命令
for (j = 0; j < server.also_propagate.numops; j++) {
rop = &server.also_propagate.ops[j];
int target = rop->target;
/* Whatever the command wish is, we honor the call() flags. */
// 執行相應傳播命令操作
if (!(flags&CMD_CALL_PROPAGATE_AOF)) target &= ~PROPAGATE_AOF;
if (!(flags&CMD_CALL_PROPAGATE_REPL)) target &= ~PROPAGATE_REPL;
if (target)
propagate(rop->cmd,rop->dbid,rop->argv,rop->argc,target);
}
}
// 釋放Redis操作數組
redisOpArrayFree(&server.also_propagate);
}
// 命令執行的次數加1
server.stat_numcommands++;
}
執行命令時,可以指定一個flags
。這個flags
是用於執行完命令之後的一些後續工作。我們說明這些flags
的含義:
CMD_CALL_NONE:沒有指定flags
CMD_CALL_SLOWLOG:檢查命令的執行速度,如果需要記錄在慢查詢日誌中
CMD_CALL_STATS:記錄命令的統計信息
CMD_CALL_PROPAGATE_AOF:如果client設置了強制傳播的標誌或修改了數據集,則將命令追加到AOF文件中
CMD_CALL_PROPAGATE_REPL:如果client設置了強制傳播的標誌或修改了數據集,則將命令發送給從節點服務器中
CMD_CALL_PROPAGATE:如果client設置了強制傳播的標誌或修改了數據集,則將命令發送給從節點服務器或追加到AOF中
CMD_CALL_FULL:包含以上所有的含義
執行命令c->cmd->proc(c)
就相當於執行了命令實現的函數,然後會在執行完成後,由這些函數產生相應的命令回覆,根據回覆的大小,會將回復保存在輸出緩衝區buf
或回覆鏈表repl
中。然後服務器會調用writeToClient()
函數來將回複寫到fd
中。詳細請看:Redis 網絡連接庫剖析。
至此,一條命令的執行過程就很清楚明瞭了。
3. Redis服務器的週期性任務
我們曾經在Redis 事件處理實現一文中說到,Redis的事件分爲文件事件(file event)
和時間事件(time event)
。時間事件雖然是晚於文件事件
執行,但是會每隔100ms
都會執行一次。話不多說直接上代碼:Redis 單機服務器實現源碼註釋
// 使用一個宏定義:run_with_period(milliseconds) { .... },實現一部分代碼有次數限制的被執行
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
int j;
UNUSED(eventLoop);
UNUSED(id);
UNUSED(clientData);
// 如果設置了看門狗,則在過期時間內,遞達一個 SIGALRM 信號
if (server.watchdog_period) watchdogScheduleSignal(server.watchdog_period);
// 設置服務器的時間緩存
updateCachedTime();
// 更新服務器的一些統計值
run_with_period(100) {
// 命令執行的次數
trackInstantaneousMetric(STATS_METRIC_COMMAND,server.stat_numcommands);
// 從網絡讀到的字節數
trackInstantaneousMetric(STATS_METRIC_NET_INPUT,
server.stat_net_input_bytes);
// 已經寫到網絡的字節數
trackInstantaneousMetric(STATS_METRIC_NET_OUTPUT,
server.stat_net_output_bytes);
}
// 服務器的LRU時間表示位數爲24位,因此最長表示2^24秒,大約1.5年,只要在1.5年內,該對象被訪問,那麼就不會出現對象的LRU時間比服務器的時鐘還要年輕的現象
// LRU_CLOCK_RESOLUTION 可以改變LRU時間的精度
// 獲取服務器的LRU時鐘
server.lruclock = getLRUClock();
// 更新服務器的最大內存使用量峯值
if (zmalloc_used_memory() > server.stat_peak_memory)
server.stat_peak_memory = zmalloc_used_memory();
// 更新常駐內存的大小
server.resident_set_size = zmalloc_get_rss();
// 安全的關閉服務器
if (server.shutdown_asap) {
// 關閉服務器前的準備動作,成功則關閉服務器
if (prepareForShutdown(SHUTDOWN_NOFLAGS) == C_OK) exit(0);
// 失敗則打印日誌
serverLog(LL_WARNING,"SIGTERM received but errors trying to shut down the server, check the logs for more information");
// 撤銷關閉服務器標誌
server.shutdown_asap = 0;
}
// 打印數據庫的信息到日誌中
run_with_period(5000) {
// 遍歷數據庫
for (j = 0; j < server.dbnum; j++) {
long long size, used, vkeys;
// 獲取當前數據庫的鍵值對字典的槽位數,鍵值對字典已使用的數量,過期鍵字典已使用的數量
size = dictSlots(server.db[j].dict);
used = dictSize(server.db[j].dict);
vkeys = dictSize(server.db[j].expires);
// 打印到日誌中
if (used || vkeys) {
serverLog(LL_VERBOSE,"DB %d: %lld keys (%lld volatile) in %lld slots HT.",j,used,vkeys,size);
/* dictPrintStats(server.dict); */
}
}
}
// 如果服務器不在哨兵模式下,那麼週期性打印一些連接client的信息到日誌中
if (!server.sentinel_mode) {
run_with_period(5000) {
serverLog(LL_VERBOSE,
"%lu clients connected (%lu slaves), %zu bytes in use",
listLength(server.clients)-listLength(server.slaves),
listLength(server.slaves),
zmalloc_used_memory());
}
}
// 執行client的週期性任務
clientsCron();
// 執行數據庫的週期性任務
databasesCron();
// 如果當前沒有正在進行RDB和AOF持久化操作,且AOF重寫操作被提上了日程,那麼在後臺執行AOF的重寫操作
if (server.rdb_child_pid == -1 && server.aof_child_pid == -1 &&
server.aof_rewrite_scheduled)
{
rewriteAppendOnlyFileBackground();
}
// 如果正在進行RDB或AOF重寫等操作,那麼等待接收子進程發來的信息
if (server.rdb_child_pid != -1 || server.aof_child_pid != -1 ||
ldbPendingChildren())
{
int statloc;
pid_t pid;
// 接收所有子進程發送的信號,非阻塞
if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) {
// 獲取退出碼
int exitcode = WEXITSTATUS(statloc);
int bysignal = 0;
// 判斷子進程是否因爲信號而終止,是的話,取得子進程因信號而中止的信號碼
if (WIFSIGNALED(statloc)) bysignal = WTERMSIG(statloc);
// 子進程沒有退出,還在進行RDB或AOF重寫等操作
if (pid == -1) {
// 打印日誌
serverLog(LL_WARNING,"wait3() returned an error: %s. "
"rdb_child_pid = %d, aof_child_pid = %d",
strerror(errno),
(int) server.rdb_child_pid,
(int) server.aof_child_pid);
// RDB持久化完成
} else if (pid == server.rdb_child_pid) {
// 將RDB文件寫入磁盤或網絡中
backgroundSaveDoneHandler(exitcode,bysignal);
// AOF持久化完成
} else if (pid == server.aof_child_pid) {
// 將重寫緩衝區的命令追加AOF文件中,且進行同步操作
backgroundRewriteDoneHandler(exitcode,bysignal);
// 其他子進程,打印日誌
} else {
if (!ldbRemoveChild(pid)) {
serverLog(LL_WARNING,
"Warning, detected child with unmatched pid: %ld",
(long)pid);
}
}
// 更新能否resize哈希的策略
updateDictResizePolicy();
}
// 沒有正在進行RDB或AOF重寫等操作,那麼檢查是否需要執行
} else {
// 遍歷save命令的參數數組
for (j = 0; j < server.saveparamslen; j++) {
struct saveparam *sp = server.saveparams+j;
// 數據庫的鍵被修改的次數大於SAVE命令參數指定的修改次數,且已經過了SAVE命令參數指定的秒數
if (server.dirty >= sp->changes &&
server.unixtime-server.lastsave > sp->seconds &&
(server.unixtime-server.lastbgsave_try >
CONFIG_BGSAVE_RETRY_DELAY ||
server.lastbgsave_status == C_OK))
{
serverLog(LL_NOTICE,"%d changes in %d seconds. Saving...",
sp->changes, (int)sp->seconds);
// 進行 BGSAVE 操作
rdbSaveBackground(server.rdb_filename);
break;
}
}
// 是否觸發AOF重寫操作
if (server.rdb_child_pid == -1 &&
server.aof_child_pid == -1 &&
server.aof_rewrite_perc &&
server.aof_current_size > server.aof_rewrite_min_size)
{
// 上一次重寫後的大小
long long base = server.aof_rewrite_base_size ?
server.aof_rewrite_base_size : 1;
// AOF文件增長的百分比
long long growth = (server.aof_current_size*100/base) - 100;
// 大於設置的百分比100則進行AOF後臺重寫
if (growth >= server.aof_rewrite_perc) {
serverLog(LL_NOTICE,"Starting automatic rewriting of AOF on %lld%% growth",growth);
rewriteAppendOnlyFileBackground();
}
}
}
// 將AOF緩存沖洗到磁盤中
if (server.aof_flush_postponed_start) flushAppendOnlyFile(0);
// 當AOF重寫操作,同樣將重寫緩衝區的數據刷新到AOF文件中
run_with_period(1000) {
if (server.aof_last_write_status == C_ERR)
flushAppendOnlyFile(0);
}
// 釋放被設置爲異步釋放的client
freeClientsInAsyncFreeQueue();
// 解除client的暫停狀態
clientsArePaused(); /* Don't check return value, just use the side effect. */
// 週期性執行復制的任務
run_with_period(1000) replicationCron();
/* Run the Redis Cluster cron. */
// 週期性執行集羣任務
run_with_period(100) {
if (server.cluster_enabled) clusterCron();
}
//週期性執行哨兵任務
run_with_period(100) {
if (server.sentinel_mode) sentinelTimer();
}
// 清理過期的被緩存的sockets連接
run_with_period(1000) {
migrateCloseTimedoutSockets();
}
// 如果 BGSAVE 被提上過日程,那麼進行BGSAVE操作,因爲AOF重寫操作在更新
// 注意:此代碼必須在上面的replicationCron()調用之後,確保在重構此文件以保持此順序時。 這是有用的,因爲我們希望優先考慮RDB節省的複製
if (server.rdb_child_pid == -1 && server.aof_child_pid == -1 &&
server.rdb_bgsave_scheduled &&
(server.unixtime-server.lastbgsave_try > CONFIG_BGSAVE_RETRY_DELAY ||
server.lastbgsave_status == C_OK))
{
// 更新執行BGSAVE,成功則清除rdb_bgsave_scheduled標誌
if (rdbSaveBackground(server.rdb_filename) == C_OK)
server.rdb_bgsave_scheduled = 0;
}
// 週期loop計數器加1
server.cronloops++;
// 返回週期,默認爲100ms
return 1000/server.hz;
}
我們也是大致總結列出部分:
- 主動刪除過期的鍵(也可以在讀數據庫時被動刪除)
- 喂看門狗 watchdog
- 更新一些統計值
- 漸進式rehash
- 觸發 BGSAVE / AOF 的重寫操作,並處理子進程的中斷
- 不同狀態的client的超時
- 複製重連
- 等……
我們重點看兩個函數,一個是關於客戶端資源管理的clientsCron()
,一個是關於數據庫資源管理的databasesCron()
。
3.1客戶端資源管理
服務器要定時檢查client是否與服務器有交互,如果超過了設置的限制時間,則要釋放client所佔用的資源。具體的函數是clientsCronHandleTimeout()
,它被clientsCron()
函數所調用。
// 檢查超時,如果client中斷超時返回非零值,函數獲取當前時間作爲參數因爲他被一個循環中調用多次。所以調用gettimeofday()爲每一次迭代都是昂貴的,而沒有任何實際的效益
// client被關閉則返回1,沒有關閉返回0
int clientsCronHandleTimeout(client *c, mstime_t now_ms) {
// 當前時間,單位秒
time_t now = now_ms/1000;
// 當前時間 - client上一次和服務器交互的時間 如果大於 服務器中設置client超過的最大時間
// 不檢查這四類client的超時時間:slaves從節點服務器、masters主節點服務器、BLPOP被阻塞的client、訂閱狀態的client
if (server.maxidletime &&
!(c->flags & CLIENT_SLAVE) && /* no timeout for slaves */
!(c->flags & CLIENT_MASTER) && /* no timeout for masters */
!(c->flags & CLIENT_BLOCKED) && /* no timeout for BLPOP */
!(c->flags & CLIENT_PUBSUB) && /* no timeout for Pub/Sub clients */
(now - c->lastinteraction > server.maxidletime))
{
serverLog(LL_VERBOSE,"Closing idle client");
freeClient(c);
return 1;
// 如果client處於BLPOP被阻塞
} else if (c->flags & CLIENT_BLOCKED) {
// 如果阻塞的client的超時時間已經到達
if (c->bpop.timeout != 0 && c->bpop.timeout < now_ms) {
// 回覆client一個空回覆
replyToBlockedClientTimedOut(c);
// 接觸client的阻塞狀態
unblockClient(c);
// 如果服務器處於集羣模式
} else if (server.cluster_enabled) {
// 重定向client的阻塞到其他的服務器
if (clusterRedirectBlockedClientIfNeeded(c))
// 解除阻塞
unblockClient(c);
}
}
return 0;
}
3.2 數據庫資源管理
服務器要定時檢查數據庫的輸入緩衝區是否可以resize
,以節省內存資源。而resize輸入緩衝區的兩個條件:
- 輸入緩衝區的大小大於32K以及超過緩衝區的峯值的2倍。
- client超過時間大於2秒,且輸入緩衝區的大小超過1k
實現的函數是clientsCronResizeQueryBuffer()
,被databasesCron()
函數所調用。
// resize客戶端的輸入緩衝區
int clientsCronResizeQueryBuffer(client *c) {
// 獲取輸入緩衝區的大小
size_t querybuf_size = sdsAllocSize(c->querybuf);
// 計算服務器對於client的空轉時間,也就是client的超時時間
time_t idletime = server.unixtime - c->lastinteraction;
// resize輸入緩衝區的兩個條件:
// 1. 輸入緩衝區的大小大於32K以及超過緩衝區的峯值的2倍
// 2. client超過時間大於2秒,且輸入緩衝區的大小超過1k
if (((querybuf_size > PROTO_MBULK_BIG_ARG) &&
(querybuf_size/(c->querybuf_peak+1)) > 2) ||
(querybuf_size > 1024 && idletime > 2))
{
// 只有輸入緩衝區的未使用大小超過1k,則會釋放未使用的空間
if (sdsavail(c->querybuf) > 1024) {
c->querybuf = sdsRemoveFreeSpace(c->querybuf);
}
}
// 清空輸入緩衝區的峯值
c->querybuf_peak = 0;
return 0;
}
4. maxmemory的策略
Redis 服務器對內存使用會有一個server.maxmemory
的限制,如果超過這個限制,就要通過刪除一些鍵空間來釋放一些內存,具體函數對應freeMemoryIfNeeded()
。Redis 單機服務器實現源碼註釋
釋放內存時,可以指定不同的策略。策略保存在maxmemory_policy
中,他可以指定以下的幾個值:
#define MAXMEMORY_VOLATILE_LRU 0
#define MAXMEMORY_VOLATILE_TTL 1
#define MAXMEMORY_VOLATILE_RANDOM 2
#define MAXMEMORY_ALLKEYS_LRU 3
#define MAXMEMORY_ALLKEYS_RANDOM 4
#define MAXMEMORY_NO_EVICTION 5
可以看出主要分爲三種,
- LRU:優先刪除最近最少使用的鍵。
- TTL:優先刪除生存時間最短的鍵。
- RANDOM:隨機刪除。
而ALLKEYS
和VOLATILE
的不同之處就是要確定是從數據庫的鍵值對字典還是過期鍵字典中刪除。
瞭解了以上這些,我們貼出代碼:
// 按需釋放內存空間
int freeMemoryIfNeeded(void) {
size_t mem_used, mem_tofree, mem_freed;
int slaves = listLength(server.slaves);
mstime_t latency, eviction_latency;
// 計算出服務器總的內存使用量,但是有兩部分要減去
/*
1、從節點的輸出緩衝區
2、AOF緩衝區
*/
mem_used = zmalloc_used_memory();
// 存在從節點
if (slaves) {
listIter li;
listNode *ln;
listRewind(server.slaves,&li);
// 遍歷從節點鏈表
while((ln = listNext(&li))) {
client *slave = listNodeValue(ln);
// 獲取當前從節點的輸出緩衝區的大小,不包含靜態的固定回覆緩衝區,因爲他總被分配
unsigned long obuf_bytes = getClientOutputBufferMemoryUsage(slave);
// 減去當前從節點的輸出緩衝區的大小
if (obuf_bytes > mem_used)
mem_used = 0;
else
mem_used -= obuf_bytes;
}
}
// 如果開啓了AOF操作
if (server.aof_state != AOF_OFF) {
// 減去AOF緩衝區的大小
mem_used -= sdslen(server.aof_buf);
// 減去AOF重寫緩衝區的大小
mem_used -= aofRewriteBufferSize();
}
// 如果沒有超過服務器設置的最大內存限制,則返回C_OK
if (mem_used <= server.maxmemory) return C_OK;
// 如果內存回收策略爲不回收,則返回C_ERR
if (server.maxmemory_policy == MAXMEMORY_NO_EVICTION)
return C_ERR; /* We need to free memory, but policy forbids. */
// 計算需要回收的大小
mem_tofree = mem_used - server.maxmemory;
// 已回收的大小
mem_freed = 0;
// 設置回收延遲檢測開始的時間
latencyStartMonitor(latency);
// 循環回收,直到到達需要回收大小
while (mem_freed < mem_tofree) {
int j, k, keys_freed = 0;
// 遍歷所有的數據庫
for (j = 0; j < server.dbnum; j++) {
long bestval = 0; /* just to prevent warning */
sds bestkey = NULL;
dictEntry *de;
redisDb *db = server.db+j;
dict *dict;
// 如果回收策略有ALLKEYS_LRU或RANDOM,從鍵值對字典中選擇回收
if (server.maxmemory_policy == MAXMEMORY_ALLKEYS_LRU ||
server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM)
{
// 則從鍵值對字典中選擇回收的鍵。選擇樣品字典
dict = server.db[j].dict;
} else {
// 否則從過期鍵字典中選擇回收的鍵。選擇樣品字典
dict = server.db[j].expires;
}
if (dictSize(dict) == 0) continue; //跳過空字典
/* volatile-random and allkeys-random policy */
// 如果回收策略有 ALLKEYS_RANDOM 或 VOLATILE_RANDOM,則是隨機挑選
if (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM ||
server.maxmemory_policy == MAXMEMORY_VOLATILE_RANDOM)
{
// 隨機返回一個key
de = dictGetRandomKey(dict);
bestkey = dictGetKey(de);
}
/* volatile-lru and allkeys-lru policy */
// 如果回收策略有 ALLKEYS_LRU 或 VOLATILE_LRU,則使用LRU策略
else if (server.maxmemory_policy == MAXMEMORY_ALLKEYS_LRU ||
server.maxmemory_policy == MAXMEMORY_VOLATILE_LRU)
{
// 回收池
struct evictionPoolEntry *pool = db->eviction_pool;
while(bestkey == NULL) {
// evictionPoolPopulate()用於在每次我們想要過期一個鍵的時候,用幾個節點填充evictionPool。 空閒時間小於當前key的之一的key被添加。 如果有free的節點,則始終添加key。 我們按升序插入key,所以空閒時間越短的鍵在左邊,右邊的空閒時間越長。
// 從樣品字典dict中隨機選擇樣品
evictionPoolPopulate(dict, db->dict, db->eviction_pool);
// 從空轉時間最長的開始遍歷
for (k = MAXMEMORY_EVICTION_POOL_SIZE-1; k >= 0; k--) {
// 跳過空位置
if (pool[k].key == NULL) continue;
// 從樣品字典dict中查找當前key
de = dictFind(dict,pool[k].key);
// 從收回池中刪除
sdsfree(pool[k].key);
// 釋放位置
memmove(pool+k,pool+k+1,
sizeof(pool[0])*(MAXMEMORY_EVICTION_POOL_SIZE-k-1));
// 重置key和空轉時間
pool[MAXMEMORY_EVICTION_POOL_SIZE-1].key = NULL;
pool[MAXMEMORY_EVICTION_POOL_SIZE-1].idle = 0;
// 如果從樣品字典中可以找到,則保存鍵
if (de) {
bestkey = dictGetKey(de);
break;
// 沒找到,則繼續找下一個樣品空間所保存的鍵
} else {
/* Ghost... */
continue;
}
}
// 如果當前選出的所有的樣品都沒找到,則重新選擇一批樣品,知道找到一個可以釋放的鍵
}
}
/* volatile-ttl */
// 如果回收策略有 VOLATILE_TTL,則選擇生存時間最短的鍵
else if (server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL) {
// 抽樣個數爲maxmemory_samples個
for (k = 0; k < server.maxmemory_samples; k++) {
sds thiskey;
long thisval;
// 返回一個鍵,獲取他的生存時間
de = dictGetRandomKey(dict);
thiskey = dictGetKey(de);
thisval = (long) dictGetVal(de);
// 如果當前鍵的生存時間更短,則保存
if (bestkey == NULL || thisval < bestval) {
bestkey = thiskey;
bestval = thisval;
}
}
}
/* Finally remove the selected key. */
// 刪除所有被選擇的鍵
if (bestkey) {
long long delta;
robj *keyobj = createStringObject(bestkey,sdslen(bestkey));
// 當一個鍵在主節點中過期時,主節點會發送del命令給從節點和AOF文件
propagateExpire(db,keyobj);
// 單獨計算dbDelete()所釋放的空間大小, 在AOF和複製鏈接中傳播DEL的內存實際上大於我們釋放的key的內存
// 但是無法解釋,竇澤不會退出循環
// AOF和輸出緩衝區的內存最終被釋放,所以我們只關心鍵空間使用的內存
delta = (long long) zmalloc_used_memory();
// 設置刪除key對象的開始時間
latencyStartMonitor(eviction_latency);
dbDelete(db,keyobj);
// 保存刪除key對象時間
latencyEndMonitor(eviction_latency);
// 添加到延遲診斷字典中
latencyAddSampleIfNeeded("eviction-del",eviction_latency);
// 刪除嵌套的延遲事件
latencyRemoveNestedEvent(latency,eviction_latency);
// 計算刪除這個鍵的大小
delta -= (long long) zmalloc_used_memory();
// 更新內存釋放量
mem_freed += delta;
// 服務器總的回收鍵的個數計數器加1
server.stat_evictedkeys++;
// 事件通知
notifyKeyspaceEvent(NOTIFY_EVICTED, "evicted",
keyobj, db->id);
// 釋放鍵對象
decrRefCount(keyobj);
// 釋放鍵的個數加1
keys_freed++;
// 如果有從節點,則刷新所有的輸出緩衝區數據
if (slaves) flushSlavesOutputBuffers();
}
}
// 如果所有數據庫都沒有釋放鍵,返回C_ERR
if (!keys_freed) {
latencyEndMonitor(latency);
latencyAddSampleIfNeeded("eviction-cycle",latency);
return C_ERR; /* nothing to free... */
}
}
// 計算回收延遲的時間
latencyEndMonitor(latency);
latencyAddSampleIfNeeded("eviction-cycle",latency);
return C_OK;
}
5. Redis服務器的main函數
Redis 服務器的mian()
主要執行了一下操作:Redis 單機服務器實現源碼註釋
- 初始化服務器狀態
- 載入服務器的配置
- 初始化服務器數據結構
- 載入持久化文件還原數據庫狀態
- 執行事件循環
int main(int argc, char **argv) {
struct timeval tv;
int j;
#ifdef INIT_SETPROCTITLE_REPLACEMENT
spt_init(argc, argv);
#endif
// 本函數用來配置地域的信息,設置當前程序使用的本地化信息,LC_COLLATE 配置字符串比較
setlocale(LC_COLLATE,"");
// 設置線程安全
zmalloc_enable_thread_safeness();
// 設置內存溢出的處理函數
zmalloc_set_oom_handler(redisOutOfMemoryHandler);
// 初始化隨機數發生器
srand(time(NULL)^getpid());
// 保存當前信息
gettimeofday(&tv,NULL);
// 設置哈希函數的種子
dictSetHashFunctionSeed(tv.tv_sec^tv.tv_usec^getpid());
// 檢查開啓哨兵模式的兩種方式
server.sentinel_mode = checkForSentinelMode(argc,argv);
// 初始化服務器配置
initServerConfig();
// 設置可執行文件的絕對路徑
server.executable = getAbsolutePath(argv[0]);
// 分配執行executable文件的參數列表的空間
server.exec_argv = zmalloc(sizeof(char*)*(argc+1));
server.exec_argv[argc] = NULL;
// 保存當前參數
for (j = 0; j < argc; j++) server.exec_argv[j] = zstrdup(argv[j]);
// 如果已開啓哨兵模式
if (server.sentinel_mode) {
// 初始化哨兵的配置
initSentinelConfig();
initSentinel();
}
// 檢查是否執行"redis-check-rdb"檢查程序
if (strstr(argv[0],"redis-check-rdb") != NULL)
redis_check_rdb_main(argc,argv); //該函數不會返回
// 解析參數
if (argc >= 2) {
j = 1; /* First option to parse in argv[] */
sds options = sdsempty();
char *configfile = NULL;
/* Handle special options --help and --version */
// 指定了打印版本信息,然後退出
if (strcmp(argv[1], "-v") == 0 ||
strcmp(argv[1], "--version") == 0) version();
// 執行幫助信息,然後退出
if (strcmp(argv[1], "--help") == 0 ||
strcmp(argv[1], "-h") == 0) usage();
// 執行內存測試程序
if (strcmp(argv[1], "--test-memory") == 0) {
if (argc == 3) {
memtest(atoi(argv[2]),50);
exit(0);
} else {
fprintf(stderr,"Please specify the amount of memory to test in megabytes.\n");
fprintf(stderr,"Example: ./redis-server --test-memory 4096\n\n");
exit(1);
}
}
/* First argument is the config file name? */
// 如果第1個參數不是'-',那麼是配置文件
if (argv[j][0] != '-' || argv[j][1] != '-') {
configfile = argv[j];
// 設置配置文件的絕對路徑
server.configfile = getAbsolutePath(configfile);
/* Replace the config file in server.exec_argv with
* its absoulte path. */
zfree(server.exec_argv[j]);
// 設置可執行的參數列表
server.exec_argv[j] = zstrdup(server.configfile);
j++;
}
// 解析指定的對象
while(j != argc) {
// 如果是以'-'開頭
if (argv[j][0] == '-' && argv[j][1] == '-') {
/* Option name */
// 跳過"--check-rdb"
if (!strcmp(argv[j], "--check-rdb")) {
/* Argument has no options, need to skip for parsing. */
j++;
continue;
}
// 每個選項之間用'\n'隔開
if (sdslen(options)) options = sdscat(options,"\n");
// 將選項追加在sds中
options = sdscat(options,argv[j]+2);
// 選項和參數用 " "隔開
options = sdscat(options," ");
} else {
/* Option argument */
// 追加選項參數
options = sdscatrepr(options,argv[j],strlen(argv[j]));
options = sdscat(options," ");
}
j++;
}
// 如果開啓哨兵模式,哨兵模式配置文件不正確
if (server.sentinel_mode && configfile && *configfile == '-') {
serverLog(LL_WARNING,
"Sentinel config from STDIN not allowed.");
serverLog(LL_WARNING,
"Sentinel needs config file on disk to save state. Exiting...");
exit(1);
}
// 重置save命令的參數
resetServerSaveParams();
// 載入配置文件
loadServerConfig(configfile,options);
sdsfree(options);
} else {
serverLog(LL_WARNING, "Warning: no config file specified, using the default config. In order to specify a config file use %s /path/to/%s.conf", argv[0], server.sentinel_mode ? "sentinel" : "redis");
}
// 是否被監視
server.supervised = redisIsSupervised(server.supervised_mode);
// 是否以守護進程的方式運行
int background = server.daemonize && !server.supervised;
if (background) daemonize();
// 初始化服務器
initServer();
// 創建保存pid的文件
if (background || server.pidfile) createPidFile();
// 爲服務器進程設置標題
redisSetProcTitle(argv[0]);
// 打印Redis的logo
redisAsciiArt();
// 檢查backlog隊列
checkTcpBacklogSettings();
// 如果不是哨兵模式
if (!server.sentinel_mode) {
/* Things not needed when running in Sentinel mode. */
serverLog(LL_WARNING,"Server started, Redis version " REDIS_VERSION);
#ifdef __linux__
// 打印內存警告
linuxMemoryWarnings();
#endif
// 從AOF文件或RDB文件載入數據
loadDataFromDisk();
// 如果開啓了集羣模式
if (server.cluster_enabled) {
// 集羣模式下驗證載入的數據
if (verifyClusterConfigWithData() == C_ERR) {
serverLog(LL_WARNING,
"You can't have keys in a DB different than DB 0 when in "
"Cluster mode. Exiting.");
exit(1);
}
}
// 打印端口號
if (server.ipfd_count > 0)
serverLog(LL_NOTICE,"The server is now ready to accept connections on port %d", server.port);
// 打印本地套接字fd
if (server.sofd > 0)
serverLog(LL_NOTICE,"The server is now ready to accept connections at %s", server.unixsocket);
} else {
// 開啓哨兵模式,哨兵模式和集羣模式只能開啓一種
sentinelIsRunning();
}
/* Warning the user about suspicious maxmemory setting. */
// 最大內存限制是否配置正確
if (server.maxmemory > 0 && server.maxmemory < 1024*1024) {
serverLog(LL_WARNING,"WARNING: You specified a maxmemory value that is less than 1MB (current value is %llu bytes). Are you sure this is what you really want?", server.maxmemory);
}
// 進入事件循環之前執行beforeSleep()函數
aeSetBeforeSleepProc(server.el,beforeSleep);
// 運行事件循環,一直到服務器關閉
aeMain(server.el);
// 服務器關閉,刪除事件循環
aeDeleteEventLoop(server.el);
return 0;
}