文章導航
Redis源碼系列的初衷,是幫助我們更好地理解Redis,更懂Redis,而怎么才能懂,光看是不夠的,建議跟着下面的這一篇,把環境搭建起來,后續可以自己閱讀源碼,或者跟着我這邊一起閱讀。由於我用c也是好幾年以前了,些許錯誤在所難免,希望讀者能不吝指出。
曹工說Redis源碼(1)-- redis debug環境搭建,使用clion,達到和調試java一樣的效果
曹工說Redis源碼(2)-- redis server 啟動過程解析及簡單c語言基礎知識補充
曹工說Redis源碼(3)-- redis server 啟動過程完整解析(中)
曹工說Redis源碼(4)-- 通過redis server源碼來理解 listen 函數中的 backlog 參數
曹工說Redis源碼(5)-- redis server 啟動過程解析,以及EventLoop每次處理事件前的前置工作解析(下)
曹工說Redis源碼(6)-- redis server 主循環大體流程解析
本講主題
本講,聚焦於redis的周期執行任務。redis啟動起來后,基本就剩下兩件事,上一講的主流程分析中,已經講到了。1個是處理客戶端請求,2就是指向周期任務。處理客戶端請求,大概會細分為:處理客戶端連接事件(客戶端連接到redis)、客戶端讀寫事件(客戶端發送請求,redis返回響應);
周期任務呢,就是本講主題,let's go。
周期任務的大體流程
周期任務,上一講已經提到,其就是一個函數指針,具體實現,就是redis.c中的 serverCron 函數。
該函數的大的流程,按照代碼中的執行順序,我們先了解下:
-
注冊一個watchdog,注冊方式是通過一個timer,注冊了該timer之后,會定期給當前進程,觸發一個
SIGALRM
信號,觸發了這個信號后,會干嘛呢,會回調位於 debug.c 文件中的watchdogSignalHandler
方法,這個方法,主要是在redis執行一些命令時,超過指定時長后,打印一些debug日志。可以參考:
-
更新server時間,redis server在很多時候,都需要獲取當前時間,就像我們寫業務代碼差不多,但是,redis比較扣,扣什么?扣性能。在不需要獲取當前時間的時候,redis覺得,獲取一個不那么准確的時間就行了。所以,就緩存了一個全局時間,這個全局時間,什么時候刷新呢,就在這個周期任務中。
大家仔細看注釋吧:
/* We take a cached value of the unix time in the global state because with * virtual memory and aging there is to store the current time in objects at * every object access, and accuracy is not needed. To access a global var is * a lot faster than calling time(NULL) */ void updateCachedTime(void) { server.unixtime = time(NULL); server.mstime = mstime(); }
簡單翻譯下,就是說,每個對象,每次被訪問的時候,有個access-time,這個時間,不需要那么精確,沒必要每次去new date(),使用緩存的時間就行了,這樣能比較快。全局時間,緩存在server.unixtime 和 server.mstime中。
-
計算redis的ops,類似於tps;這個操作,不是每次該周期任務時,都要執行,而是自定義執行的周期,總體來說,沒有本周期任務那么頻繁。
redis中,定義了一個宏來實現這個功能,比如:
// 記錄服務器執行命令的次數 run_with_period(100) trackOperationsPerSecond();
這個就是,每100ms執行一次上面的這個操作。
這個怎么去計算ops(operation per second)呢?看下面的代碼即懂:
void trackOperationsPerSecond(void) { // 計算兩次抽樣之間的時間長度,毫秒格式 long long t = mstime() - server.ops_sec_last_sample_time; // 計算兩次抽樣之間,執行了多少個命令 long long ops = server.stat_numcommands - server.ops_sec_last_sample_ops; long long ops_sec; //1 計算距離上一次抽樣之后,每秒執行命令的數量 ops_sec = t > 0 ? (ops * 1000 / t) : 0; ... }
1處,分子分母,大家一看,應該就懂了。ops = 一段時間內的操作數量/ 時間長度。
-
刷新服務器的 LRU 時間,目前,我覺得可以簡單理解為:redis的空間大小是有限的,假設機器內存10g,那么不可能把數據庫的幾個t的數據都放redis,所以基本是放熱數據,那不熱的數據怎么辦?被清除。清除的算法,就是lru。每個key,不管設沒設過期時間,都會維護一個lruClock,即最近一次被訪問的時間。
計算一個對象的空閑時長,就是用服務器的LRU時間 減去 key的LRU時間。
// 使用近似 LRU 算法,計算出給定對象的閑置時長 unsigned long long estimateObjectIdleTime(robj *o) { unsigned long long lruclock = LRU_CLOCK(); if (lruclock >= o->lru) { return (lruclock - o->lru) * REDIS_LRU_CLOCK_RESOLUTION; } else { return (lruclock + (REDIS_LRU_CLOCK_MAX - o->lru)) * REDIS_LRU_CLOCK_RESOLUTION; } }
網上的一篇文章寫得不錯,可以參考:
-
記錄服務器的內存峰值
/* Record the max memory used since the server was started. */ // 記錄服務器的內存峰值 if (zmalloc_used_memory() > server.stat_peak_memory) server.stat_peak_memory = zmalloc_used_memory();
什么時候用呢?好像只在info命令里看到使用了。
-
判斷服務器的關閉標識是否打開,如打開,則關閉
// 服務器進程收到 SIGTERM 信號,關閉服務器 if (server.shutdown_asap) { // 嘗試關閉服務器 if (prepareForShutdown(0) == REDIS_OK) exit(0); }
-
打印數據庫的鍵值對信息、客戶端信息
單純的log操作,唯一注意的是,要把日志級別調到
REDIS_VERBOSE
才看得到 -
檢查客戶端空閑時長,關閉空閑超時的客戶端
int clientsCronHandleTimeout(redisClient *c) { // 獲取當前時間 time_t now = server.unixtime; // 服務器設置了 maxidletime 時間 if (server.maxidletime && ... // 客戶端最后一次與服務器通訊的時間已經超過了 maxidletime 時間 (now - c->lastinteraction > server.maxidletime)) { redisLog(REDIS_VERBOSE, "Closing idle client"); // 關閉超時客戶端 freeClient(c); return 1; } ... }
-
對數據庫執行各種操作
/* This function handles 'background' operations we are required to do * incrementally in Redis databases, such as active key expiring, resizing, * rehashing. */ // 對數據庫執行刪除過期鍵,調整大小,以及主動和漸進式 rehash void databasesCron(void)
看注釋可知,大概有如下工作:刪除過期key,hash表的rehash,hash的size調整(如果字典的使用率低,會縮小其占用的內存大小)
后續會詳解這部分。
-
如果當前沒有aof或者rdb后台任務正在執行,且server之前被schedule了一個aof rewrite后台任務,則執行
aof 重寫。(aof記錄了每一條命令,時間長了,會重復,比如先把key a設為1,再設為2,再設為3,這樣,aof中有3條記錄,實際上,只需要一條即可,所以會重寫)
aof 重寫在一個子進程中進行,子進程完成后,會給當前進程發送信號,所以,當前進程會一直等待信號,等待子進程完成后,自己再做些處理。
比如,主進程要做什么處理呢?在 aof 重寫期間,主進程可能還是要不斷地處理命令(這里不會無限期等待,這次等不到就到下一次周期任務時再等),這期間,處理的命令,不能記錄到aof文件中,免得影響正在進行aof 重寫的子進程,所以,主進程會把這期間的命令,記錄到一個小本本上。
等到子進程寫完了,主進程再把小本本上的aof命令,寫到aof日志文件里。
-
如果當前沒有aof或者rdb后台任務在執行,也沒有被schedule 一個aof rewrite任務,那么,上面這步中的全部操作,都不會發生。
此時,會去檢查,當前是否滿足aof 重寫、rdb 保存的條件。
比如,rdb不是一般需要配置如下參數嗎:
save 900 1 save 300 10 save 60 10000
此時,就會去檢查,這些參數,是否滿足,如果滿足,就要開始進行rdb后台保存。
或者,當以下的aof參數滿足時,也會觸發aof重寫:
auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb
-
根據配置的aof fsync策略,決定是否要刷新到文件中
前面我們說的aof寫日志文件,不一定真的就寫入了文件,可能還在OS cache中,需要調用 fsync 才能寫入到文件中。
這里即對應配置文件中的:
# appendfsync always appendfsync everysec # appendfsync no
默認每秒執行一次fsync,性能和數據安全性的折衷。
-
涉及slave、cluster、sentinel的部分操作
如果運行在以上幾種模式下,會涉及到對應的一些周期操作,后續再涉及這塊。
總結
本講的主題大概是這些,其中,細節部分,比如數據庫的周期任務等,留待下講繼續。