Redis爲何這麼快?

| 做者 吳顯堅,騰訊雲數據庫高級工程師,參與過360開源項目Pika的研發工做,現從事redis數據庫研發工做。redis


Redis服務器是一個事件驅動程序, 事件是Redis服務器的核心, 它處理兩項重要的任務, 一個是IO事件(文件事件), 另一個是時間事件. Redis服務器經過套接字與客戶端進行鏈接, 而文件事件能夠理解爲服務器對套接字操做的抽象. 服務器與客戶端的通訊會產生相應的文件事件, 而服務器則經過監聽並處理這些事件來完成一系列網絡通訊操做. 另外Redis內部有一些操做(從Redis4.0的代碼分析目前時間事件只有serverCron)須要在給定的時間點執行, 而時間事件就是Redis服務器對這類定時操做的抽象。數據庫

1、aeEventLoop數組

在分析具體代碼以前, 咱們先了解一下在事件處理中處於核心部分的aeEventLoop究竟是什麼:安全

/* State of an event based program */

建立aeEventLoop只須要一個setsize參數, 它標識了當前aeEventLoop最大能夠監聽的文件描述符數(一般redis傳入server.maxclients+CONFIG_FDSET_INCR,也就是在用戶指定的最大客戶端鏈接數的基礎上再額外增長128, 這128能夠用於Redis內部打開AOF,RDB文件以及主從, 集羣互相通訊所對應的文件句柄), 建立aeEventLoop時, aeFileEvent和aeFiredEvent數組的大小就由setsize肯定。服務器

1. aeFileEvent網絡

內部以掩碼的形式存儲了當前套接字關心的事件(可讀/可寫事件), 內部還有兩個函數指針指向可讀/可寫事件發生時應該調用的函數, 另外還有一個無類型的指針指向相關聯的數據, 這裏須要注意的是, events是一個數組, 而套接字就是做爲下標來進行索引對應aeFileEvent, 例如我當前關心的套接字是9, 那麼events[9]就是它對應的文件事件數據結構(csapp中提到過, 當咱們調用系統函數返回描述符數字時, 返回的描述符老是在進程中當前沒有打開的最小描述符, 因此咱們無需擔憂文件描述符被反覆的建立銷燬, 而愈來愈大的問題)。數據結構

2. aeFiredEventapp

內部以掩碼的形式存儲了當前已經觸發的事件和對應的套接字, 實際上fired數組只有在調用aeApiPoll的時候纔會被賦值, 例如當前發現有套接字6, 8有可讀事件, 而套接字10有可寫事件, 那麼fired數組的前三個元素會被賦值{fd = 6, mask =AE_READABLE}, {fd = 8, mask = AE_READABLE}, {fd = 10, mask = AE_WRITABLE}, 緊接着咱們以6爲索引, 找到文件事件數據結構events[6],而後發現觸發的是可讀事件, 咱們再調用events[6]中rfileProc來處理可讀事件。函數

aeEventLoop *aeCreateEventLoop(int setsize){

對於時間事件, aeEventLoop中有一個timeEventHead指針指向第一個時間事件, 因爲aeEventLoop建立之初, 內部沒有任什麼時候間事件, 因此初始化時timeEventHead指向NULL, 每當有新的時間事件時, 總會被添加到timeEventHead頭部, 因爲aeTimeEvent結構體中有next指針能夠指向下一個aeTimeEvent結構體, 因此只要咱們獲取timeEventHead就能遍歷當前全部的時間事件了, 另外有一個細節須要注意, 最後一個aeTimeEvent結構體中的next指針指向的是timeEventHead, 因此全部時間事件實際上就是由一個環形鏈表串連起來的。oop

image.png

2、文件事件

在介紹中有提到過文件事件實際上就是服務器對套接字操做的抽象, 當套接字有可讀\寫事件觸發的時候, 咱們須要調用相應的處理函數, 下面先看一下跟文件事件相關的結構體:

/* File event structure */

在aeEventLoop初始化的時候會爲aeFileEvent數組(events)分配空間, 數組的大小由參數setsize指定,代表了當前Redis最大打開的套接字的大小, 套接字與aeFileEvent一一對應, 也就是說咱們能夠經過套接字數值做爲索引到events數組中找到他對應的aeFileEvent對象。

當咱們在aeEventLoop中註冊一個文件事件時, 首先咱們判斷傳入的套接字對events數組是否有越界行爲, 若沒有越界行爲, 咱們即可以獲取與當前套接字對應的aeFileEvent對象, 而後調用aeApiAddEvent將當前的文件描述符以及監聽的事件註冊到底層IO多路複用機制(epoll, select, evport, kqueue其中之一)中, 另外咱們還須要指定當可讀/可寫事件發生時須要調用的函數,另外當前文件事件的一些私有數據被存放在clientData指向的對象當中。

int aeCreateFileEvent(aeEventLoop *eventLoop,int fd, int mask,

3、時間事件

Redis內部的時間事件實際能夠分爲兩類, 一類是定時事件, 也就是須要在將來某一個時間點觸發的事件(只觸發一次), 另一類是週期性事件,和前面的定時事件只觸發一次不一樣, 週期性事件是每隔一段時間又會從新觸發一次。

Redis使用了timeProc指向函數的返回值來判斷當前屬於哪類事件, 若函數返回AE_NOMORE(也就是-1),說明當前事件無需再次觸發(將id置刪除標記AE_DELETED_EVENT_ID), 若函數返回一個大於等於0的值n, 說明再等待n秒, 該事件須要再從新被觸發(根據返回值更新when_sec和when_ms),在博客開頭提到的serverCron時間事件實際上就是一個週期性事件, 函數末尾會返回1000/server.hz, server.hz默認被設置爲10, 也就是說serverCron平均每間隔100ms會被調用一次。

/* Time event structure */

Redis調用aeCreateTimeEvent來建立一個時間任務, 實現很是簡單, 傳參咱們關注一下milliseconds和proc便可, 前者指定了時間事件距離當前的觸發時間, 後者指定了時間事件觸發時應調用的函數, 內部經過aeAddMillisecondsToNow將當前定時任務觸發的時間戳計算出來賦值給when_sec和when_ms, 而後再將timeProc指向時間事件到達時應該調用的函數。

在完成了aeTimeEvent結構體內部變量賦值以後, 最後將其添加到aeEventLoop內部的存儲定時間事件的環形鏈表的頭部中(這裏須要注意的是, 因爲咱們老是將新的時間事件加入環形鏈表的頭部, 因此時間事件觸發的時間前後並非在環形鏈表中有序的, 咱們須要將環形鏈表遍歷完畢才能保證當前已經到達的時間事件都已經被處理完畢, 不過因爲在開頭提到過, 目前Redis只存在serverCron一個時間事件, 因此咱們無需擔憂遍歷環形鏈表影響服務性能), 此時一個時間事件就算建立完成了。

static void aeAddMillisecondsToNow(long longmilliseconds, long *sec, long *ms) {

Redis經過aeDeleteTimeEvent函數來刪除一個時間任務, 傳參只有一個待刪除時間事件的id, 咱們發現這裏的刪除其實是一種惰性刪除, 將aeTimeEvent中的id標記爲AE_DELETED_EVENT_ID, 而不是直接將aeTimeEvent對象從鏈表中刪除而且釋放, 我的認爲這麼實現的緣由更可能是爲了安全考慮以及代碼的簡潔性, 考慮在一個時間事件中原本想刪除另一個時間事件, 可是因爲id填錯, 誤刪成本身了, 此時若是釋放自身aeTimeEvent對象, 這是十分危險的。

int aeDeleteTimeEvent(aeEventLoop *eventLoop,long long id)

4、事件的調度與執行

Redis是單線程的, 內部是一直處於aeMain中的while循環中, 而循環內部不斷調用aeProcessEvents函數, 該函數會對上面提到的文件事件和時間事件進行調度, 決定什麼時候處理文件事件以及時間事件。

void aeMain(aeEventLoop *eventLoop) {

實際上aeProcessEvents函數內部作的事情也很是簡單, 下面進行了梳理:

1. 首先調用aeSearchNearestTimer獲取到達時間距離當前最近的時間事件;

2. 計算上一步獲取到的時間事件還有多久才能夠觸發, 而且將結果記錄到一個struct timeval*指針指向的結構體中(若在步驟一中沒有獲取到時間事件對象, 那麼指針爲NULL);

3. 阻塞並等待文件事件的產生, 最大的阻塞時間由步驟二決定(步驟二指針爲NULL的場景表示當前沒有時間事件, 咱們能夠永遠阻塞, 直到有文件事件到達);

4. 若是在最大阻塞時間內獲取到了文件事件, 則根據文件事件的類型調用對應的讀事件處理函數或者寫事件處理函數;

5. 遍歷時間事件鏈表, 在這個過程當中可能會遇到id爲AE_DELETED_EVENT_ID的表明已經作了刪除標記的時間事件, 須要將該時間事件從鏈表中移除, 而且進行釋放, 如遇到已經達到的時間事件, 則調用其綁定的處理函數, 而且根據返回值來判斷該事件時間是否須要在給定的時間內再從新觸發。

5、問題

Q1: 時間事件觸發的時間必定精準麼?

A1: 時間事件的觸發並不能在指定的時間精準觸發, 通常都要比指定的時間稍晚一點, 此外在Redis單線程模型下, 時間事件都是串行執行的, 中間若是某個時間事件處理時間長, 更加影響了後面時間事件執行時間的精準性. 並且時間事件鏈表是無序的, 因此在極端場景下, 存在優先級低的時間事件比優先級高的事件先觸發的可能性, 不過好在目前Redis內部只有一個時間事件, 因此影響不會太大.


Q2: aeEventLoop在建立之初就指定了可監聽文件描述符的數量, 以後又經過config set maxclients命令動態調整客戶端最大鏈接數是怎麼實現的?

A2: 經過翻看源碼瞭解到, aeEventLoop提供了aeResizeSetSize函數, 用戶從新分配events和fired數組的大小, 使aeEventLoop可監聽的套接字數量得以調整, 當新的maxclients比原先要大的時候, 會調用該函數, 擴大aeEventLoop可監聽文件描述符的數量, 以支持更多的客戶端鏈接.

int aeResizeSetSize(aeEventLoop *eventLoop,int setsize) {

6、總結

Redis對事件的處理方式十分巧妙, 文件事件和時間事件之間相互配合, 充分的利用時間事件達到以前的這段時間等待和處理文件事件, 這樣既避免了CPU的空轉檢查, 也能及時的處理文件事件. 此外經過時間事件中timeProc函數的返回值, 將時間事件的移除和再次觸發權徹底交給了用戶, 使用起來更加靈活.

本文由博客一文多發平臺 OpenWrite 發佈!

相關文章
相關標籤/搜索