Redis如何高效實現定時任務

寫在文章開頭

redis通過單線程結合非阻塞事件輪詢機制實現高效的網絡IO和時間事件處理,這篇文章我們將從源碼的角度深入分析一下redis時間事件的設計與實現。

在這里插入圖片描述

Hi,我是 sharkChili ,是個不斷在硬核技術上作死的 java coder ,是 CSDN的博客專家 ,也是開源項目 Java Guide 的維護者之一,熟悉 Java 也會一點 Go ,偶爾也會在 C源碼 邊緣徘徊。寫過很多有意思的技術博客,也還在研究并輸出技術的路上,希望我的文章對你有幫助,非常歡迎你關注我的公眾號: 寫代碼的SharkChili

因為近期收到很多讀者的私信,所以也專門創建了一個交流群,感興趣的讀者可以通過上方的公眾號獲取筆者的聯系方式完成好友添加,點擊備注 “加群” 即可和筆者和筆者的朋友們進行深入交流。

在這里插入圖片描述

詳解redis中的時間事件

時間事件的定義

時間事件可以是單次到期執行銷毀,也可以是定時任務,對此redis對于時間事件統一封裝為aeTimeEvent對象,通過id來唯一標識一個事件,結合when_secwhen_ms記錄任務到期執行的秒和分,而執行時間事件的函數也是交由timeProc指針所指向的函數執行。
我們以一個redis定時執行的任務為例,如下所示,該結果通過when_secwhen_ms記錄秒之前的時間和毫秒的時間,一旦這個時間到了就會執行timeProc這個函數指針所指向的方法serverCron,該函數會定期執行各種任務,這一點筆者會在后文展開:

在這里插入圖片描述

對應的我們給出時間事件的代碼描述,即位于ae.h這個頭文件中的aeTimeEvent 結構體,這就是對時間事件的封裝結構體,可以看到它除了筆者上述提到的核心字段以外,還有一個next指針用于連接下一個注冊的時間事件:

//時間事件
typedef struct aeTimeEvent {//時間事件的id全局遞增long long id; /* time event identifier. */long when_sec; /* seconds *///時間到達的時間long when_ms; /* milliseconds *///對應時間時間的處理器aeTimeProc *timeProc;//......//連接下一個時間時間struct aeTimeEvent *next;
} aeTimeEvent;

上文提到redis的時間事件是以鏈表的形式關聯起來,這里我們也給出時間時間統一管理對象,即時間輪詢器aeEventLoop ,它通過timeEventHead記錄第一個時間時間而后續的時間時間統一用時間時間的next指針進行管理:

在這里插入圖片描述

對應我們也給出這段時間代碼的定義,即位于ae.haeEventLoop 的定義:

typedef struct aeEventLoop {//......//管理時間事件的列表aeTimeEvent *timeEventHead;//......
} aeEventLoop;

注冊時間事件

redis在服務器初始化階段,會注冊一個定時的時間事件,大約每1毫秒觸發一次,該事件主要做的是:

  1. 更新redis全局時鐘,該時鐘用于全局變量獲取時間用的。
  2. 隨機抽取redis內存數據庫中的樣本刪除過期的鍵值對。
  3. 如果檢查到aof重寫完成,則進行刷盤操作。
  4. 如果發現當前aof大小過大,則fork子進程進行aof重寫操作。
  5. …。

在這里插入圖片描述

對應我們給出時間事件注冊的源碼段,即redis初始化時調用的方法initServer中的aeCreateTimeEvent,可以看到它將定時任務封裝為時間事件timeEvent,并設置時間間隔為1毫秒一次:

void initServer(void) {//....../* Create the serverCron() time event, that's our main way to process* background operations. *///創建時間事件注冊到eventLoop->timeEventHead中if(aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {redisPanic("Can't create the serverCron time event.");exit(1);}//......
}

輪詢處理時間事件

redis每次處理完所有用戶的請求之后,都會調用一次時間時間處理函數processTimeEvents,輪詢并處理就緒的時間事件,由此保證盡可能準時執行時間事件,如果事件時間非定時任務則執行完成直接刪除,反之設置下一次執行時間。這些步驟全部完成之后,返回本次處理的時間事件數:

在這里插入圖片描述

我們給出處理時間循環的入口aeMain,可以看到該函數就是redis核心函數所在,它會循環調用aeProcessEvents處理各種事件:

void aeMain(aeEventLoop *eventLoop) {eventLoop->stop = 0;while (!eventLoop->stop) {if (eventLoop->beforesleep != NULL)eventLoop->beforesleep(eventLoop);//處理各種事件    aeProcessEvents(eventLoop, AE_ALL_EVENTS);}
}

不如aeProcessEvents可以看到該函數執行完所有用戶請求之后調用processTimeEvents方法獲取并執行就緒的時間事件:

int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{//......//處理就緒的客戶端事件numevents = aeApiPoll(eventLoop, tvp);for (j = 0; j < numevents; j++) {aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];int mask = eventLoop->fired[j].mask;int fd = eventLoop->fired[j].fd;int rfired = 0;/* note the fe->mask & mask & ... code: maybe an already processed* event removed an element that fired and we still didn't* processed, so we check if the event is still valid. */if (fe->mask & mask & AE_READABLE) {rfired = 1;fe->rfileProc(eventLoop,fd,fe->clientData,mask);}if (fe->mask & mask & AE_WRITABLE) {if (!rfired || fe->wfileProc != fe->rfileProc)fe->wfileProc(eventLoop,fd,fe->clientData,mask);}processed++;}}//上述核心網絡IO事件完成后處理時間事件if (flags & AE_TIME_EVENTS)processed += processTimeEvents(eventLoop);return processed; /* return the number of processed file/time events */
}

最后我們就可以看到處理時間事件的核心代碼段,其內部會從timeEventHead開始輪詢就緒的時間事件,比對當前時間是否大于或者等于到期時間,如果是則執行當前時間事件,再判斷這個事件是否是定時事件,如果是則更新下次執行時間,反之刪除,最后累加本次處理的時間時間數:

static int processTimeEvents(aeEventLoop *eventLoop) {int processed = 0;aeTimeEvent *te;long long maxId;time_t now = time(NULL);//......if (now < eventLoop->lastTime) {//從時間事件頭開始te = eventLoop->timeEventHead;while(te) {te->when_sec = 0;te = te->next;}}eventLoop->lastTime = now;te = eventLoop->timeEventHead;maxId = eventLoop->timeEventNextId-1;//循環處理到期的時間事件while(te) {long now_sec, now_ms;long long id;if (te->id > maxId) {te = te->next;continue;}aeGetTime(&now_sec, &now_ms);//如果現在的事件大于到達時間if (now_sec > te->when_sec ||(now_sec == te->when_sec && now_ms >= te->when_ms)){int retval;id = te->id;//調用時間時間函數處理該事件retval = te->timeProc(eventLoop, id, te->clientData);//更新處理數processed++;//.....if (retval != AE_NOMORE) {//如果事件類型不是AE_NOMORE則說明是定時事件更新周期,反之刪除aeAddMillisecondsToNow(retval,&te->when_sec,&te->when_ms);} else {aeDeleteTimeEvent(eventLoop, id);}te = eventLoop->timeEventHead;} else {te = te->next;}}return processed;
}

redis對于時間事件實現上的優化

因為時間事件有些要求定期執行,所以redis為了保證時間執行的實時性,做了如下兩個優化:

  1. 對于比較耗時的時間事件,例如AOF重寫,通過fork子進程異步完成:
  2. 對于返回給客戶端套接字的內容,如果長度超過預設的值,會主動讓出線程執行權,避免時間時間饑餓。

在這里插入圖片描述

對應的我們給出第一點時間時間對于aof重寫的核心代碼段,可以看到serverCron內部判斷如果當前沒有rdb和aof子進程,且需要進行aof重寫則調用rewriteAppendOnlyFileBackground函數fork子進程進行aof重寫:

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {//....../* Start a scheduled AOF rewrite if this was requested by the user while* a BGSAVE was in progress. *///aof_rewrite_scheduled設置為1,且沒有其他持久化子進程則進行aof重寫,通過異步避免耗時if (server.rdb_child_pid == -1 && server.aof_child_pid == -1 &&server.aof_rewrite_scheduled){rewriteAppendOnlyFileBackground();}//......
}//fork子進程進行aof重寫
int rewriteAppendOnlyFileBackground(void) {//......if ((childpid = fork()) == 0) {//fork子進程進行aof重寫char tmpfile[256];/* Child */closeListeningSockets(0);redisSetProcTitle("redis-aof-rewrite");//生成一個tmp文件snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof", (int) getpid());if (rewriteAppendOnlyFile(tmpfile) == REDIS_OK) {//重寫aofsize_t private_dirty = zmalloc_get_private_dirty();//......exitFromChild(0);} else {exitFromChild(1);}} else {//......}return REDIS_OK; /* unreached */
}

而回復給客戶端結果的處理器sendReplyToClient內部也有一段,判斷如果寫入數totwritten 大于REDIS_MAX_WRITE_PER_EVENT (宏定義為64M),則直接中止寫入,break退出等到下一次循環處理,避免因為這個處理導致其他時間事件饑餓而導致事件執行延期:

void sendReplyToClient(aeEventLoop *el, int fd, void *privdata, int mask) {//......while(c->bufpos > 0 || listLength(c->reply)) {//......//對于文件事件數據寫入超長會讓出執行權讓時間事件能夠盡可能的執行server.stat_net_output_bytes += totwritten;if (totwritten > REDIS_MAX_WRITE_PER_EVENT &&(server.maxmemory == 0 ||zmalloc_used_memory() < server.maxmemory)) break;}//......
}

小結

以上便是筆者從源碼角度對于redis時間事件設計與實現的全部分析,希望對你有幫助。

我是 sharkchiliCSDN Java 領域博客專家開源項目—JavaGuide contributor,我想寫一些有意思的東西,希望對你有幫助,如果你想實時收到我寫的硬核的文章也歡迎你關注我的公眾號: 寫代碼的SharkChili
因為近期收到很多讀者的私信,所以也專門創建了一個交流群,感興趣的讀者可以通過上方的公眾號獲取筆者的聯系方式完成好友添加,點擊備注 “加群” 即可和筆者和筆者的朋友們進行深入交流。

在這里插入圖片描述

參考

《redis設計與實現》

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/43651.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/43651.shtml
英文地址,請注明出處:http://en.pswp.cn/web/43651.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

項目三層架構詳情

三層架構 三層架構就是為了符合“高內聚&#xff0c;低耦合”思想&#xff0c;把各個功能模塊劃分為表示層&#xff08;UI&#xff09;、業務邏輯層&#xff08;BLL&#xff09;和數據訪問層&#xff08;DAL&#xff09;三層架構&#xff0c;各層之間采用接口相互訪問&#xf…

(正向)代理 vs. 反向代理

&#xff08;正向&#xff09;代理 vs. 反向代理 代理和反向代理都是針對用戶而言的。 一、&#xff08;正向&#xff09;代理——代理客戶端 1. 流程 代理會隱藏客戶端的真實信息&#xff08;IP、端口&#xff09;&#xff0c;代替客戶端在互聯網上發起請求&#xff0c;并將…

09:C語言進階篇一

C語言進階篇一 數據類型1.1、內存占用與sizeof運算符1.2、有符號數和無符號數1.3、整形數和浮點型數存儲方式1.4、數據類型轉換1.4.1、隱式轉換1.4.2、強制轉換 數據類型 基本數據類型&#xff1a;char&#xff0c;short&#xff0c;int&#xff0c;long&#xff0c;float&…

什么是RLHF(基于人類反饋的強化學習)?

什么是RLHF&#xff08;基于人類反饋的強化學習&#xff09;&#xff1f; 基于人類反饋的強化學習&#xff08;Reinforcement Learning from Human Feedback, RLHF&#xff09;是一種結合強化學習和人類反饋的技術&#xff0c;用于訓練智能體&#xff0c;使其行為更符合人類期…

哪些類型的工作需要六西格瑪綠帶培訓?

一、六西格瑪綠帶是什么&#xff1f; 首先&#xff0c;讓我們來了解一下六西格瑪綠帶。六西格瑪綠帶是六西格瑪管理體系中的一個重要角色&#xff0c;他們通常負責在項目中執行六西格瑪方法和工具&#xff0c;協助黑帶完成復雜的項目任務。綠帶需要掌握基本的六西格瑪知識和技…

OpenJudge | 最高的分數

目錄 描述輸入輸出樣例輸入樣例輸出思路方法一方法二 CodeCC 總時間限制: 1000ms 內存限制: 65536kB 描述 孫老師講授的《計算概論》這門課期中考試剛剛結束&#xff0c;他想知道考試中取得的最高分數。因為人數比較多&#xff0c;他覺得這件事情交給計算機來做比較方便。你能…

蘿卜快跑:未來出行的雙刃劍

歡迎來到 破曉的歷程的 博客 ??不負時光&#xff0c;不負己?? 在這個日新月異的科技時代&#xff0c;無人駕駛技術正以前所未有的速度改變著我們的出行方式。蘿卜快跑&#xff0c;作為自動駕駛出租車領域的佼佼者&#xff0c;其出現無疑為城市交通注入了新的活力&#xff…

如何在在system_real_robot.launch修改訂閱的雷達

在 system_real_robot.launch 文件中修改訂閱的雷達,以使用開源 SLAM 包(如 FastLIO 和 TARE)輸出的優化后雷達話題。可以讓你的系統使用這些 SLAM 包提供的高精度雷達數據。 假設你的 Launch 文件中包括這一行: xml <param name="registeredScanTopic" ty…

Kylin系列(六)查詢優化:提升 Kylin 查詢性能

目錄 1. Kylin查詢優化的基礎知識 1.1 Kylin的架構概述 1.2 Cube的構建與存儲 2. 索引設計與優化 2.1 選擇適當的維度和度量 2.2 使用層級維度 2.3 使用字典編碼 3. 查詢改寫與優化 3.1 選擇合適的查詢語法 3.2 避免不必要的計算 3.3 使用過濾條件 4. Cube設計優化…

政企單位光纖資源高效管理與優化策略

引言 隨著信息技術的飛速發展&#xff0c;政企單位對于通信基礎設施的管理要求日益提高。然而&#xff0c;傳統的管理模式&#xff0c;如Excel表格記錄和紙質審批流程&#xff0c;已難以滿足當前復雜多變的業務需求。在此背景下&#xff0c;我們實施了光纖管理的數字化轉型項目…

雙棧實現一個隊列

兩個棧可實現將列表倒序&#xff1a;設有含三個元素的棧 A [1,2,3] 和空棧 B [] 。若循環執行 A 元素出棧并添加入棧 B &#xff0c;直到棧 A 為空&#xff0c;則 A [] , B [3,2,1] &#xff0c;即棧 B 元素為棧 A 元素倒序。 利用棧 B 刪除隊首元素&#xff1a;倒序后&am…

自定義異步線程服務

異步線程池配置&#xff1a; /*** 啟動異步線程-并配置線程池*/ Configuration EnableAsync public class AsyncConfig {Bean(name "taskExecutor")public Executor taskExecutor() {ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor();executor.setC…

玩轉springboot之SpringBoot單元測試

SpringBoot單元測試 spring單元測試 之前在spring項目中使用單元測試時是使用注解RunWith(SpringJUnit4ClassRunner.class)來進行的 RunWith(SpringJUnit4ClassRunner.class)// 通過自動織入從應用程序上下文向測試本身注入beanWebAppConfiguration // 指定web環境ContextConfi…

電商出海第一步,云手機或成重要因素

電商出海第一步并非易事&#xff0c;挑戰和機遇并存&#xff0c;出海企業或個人或將借助云手機從而達成商業部署全球化的目的&#xff1b; 下面我們從網絡穩定、數據安全、成本、以及多平臺適配方面來看&#xff0c;究竟為什么說云手機會成為出海的重要因素&#xff1b; 首先…

新手前端系列入門-什么是前端開發

一、什么是前端 前端&#xff0c;也稱為前端開發或客戶端開發&#xff0c;一般是指在構建網站或Web應用程序時&#xff0c;與用戶直接交互的部分。就是指那些我們在網頁上能看到、能直接跟用戶打交道的部分。 簡單來說&#xff0c;就是你打開一個網站&#xff0c;能看到的所有…

西門子大手筆又買一家公司,2024年“兩買”和“兩賣”的背后……

導語 大家好&#xff0c;我是社長&#xff0c;老K。專注分享智能制造和智能倉儲物流等內容。 新書《智能物流系統構成與技術實踐》 更多的海量【智能制造】相關資料&#xff0c;請到智能制造online知識星球自行下載。 今年&#xff0c;這家全球工業巨頭不僅精準出擊&#xff0c…

第4章 引擎提供的著色器工具函數和數據結構

4.1 UnityShaderVariables.cginc文件中的著色器常量和函數 4.1.1 進行變換操作用的矩陣 1.判斷USING DIRECTIONAL LIGTH宏是否定義并分析與立體渲染相關的宏 立體多例化渲染技術的核心思想是一次向渲染管道上提交兩份待渲染的幾何體數據&#xff0c;減少繪制調用&#xff08;d…

【信創國產化】Nacos 2.3.2連接達夢數據庫

JeecgBoot 目前提供的nacos版本號 2.3.2已經支持與達夢數據庫對接。 jeecg-boot/jeecg-server-cloud/jeecg-cloud-nacos項目默認加入了達夢驅動和yml配置。如果你是老代碼&#xff0c;可以參考下面的步驟手工集成 項目地址&#xff1a;https://github.com/jeecgboot/JeecgBoot…

Anaconda 安裝與基本使用總結

最近需要在服務器上安裝和使用aconada&#xff0c;發現之前總是在網上找教程&#xff0c;每次都要找&#xff0c;很麻煩。這次就自己寫一個安裝筆記。以備日后使用。 1.服務器系統版本 ubuntu22.04 2. 軟件安裝 aconda軟件的安裝可以下面的教程&#xff08;實測有效&#xf…

斐波那契查找算法

斐波那契查找原理&#xff0c;僅僅改變了中間結點(mid)的位置&#xff0c;mid不再是中間或插值得到,而是位于黃金分割點附近&#xff0c;即midlowF(k-1)-1(F代表斐波那契數列) F[k]F[k-1]F[k-2],>(F[k]-1) (F[k-1]-1)(F[k-2]-1)1 說明:只要順序表的長度為F[k]-1,則可以將該…