資料庫
伺服器中的資料庫
Redis伺服器將所有資料庫都儲存在伺服器狀態redis.h/redisServer結構的db陣列中,db陣列的每一個項都是一個redis.h/redisDb結構,每個redisDb結構代表一個數據庫。在初始化伺服器時,程式會根據伺服器狀態的dbnum屬性來決定應該建立多少個數據庫,預設情況下為16。
切換資料庫
每個Redis客戶端都有自己的目標資料庫,每當客戶端執行資料庫寫命令或者讀命令時,目標資料庫就會成為這些命令操作的物件。預設情況下,Redis客戶端的目標資料庫為0號資料庫,但客戶端可以透過執行SELECT命令來切換目標資料庫,如下圖所示。
在伺服器內部,客戶端狀態redisClient結構的db屬性記錄了客戶端當前的目標資料庫,這個屬性是一個指向redisDb結構的指標。
資料庫鍵空間Redis是一個鍵值對資料庫伺服器,伺服器中的每個資料庫都由一個redis.h/redisDb結構表示,其中,redisDb結構的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; long long avg_ttl; /* Average TTL, just for stats */} redisDb;複製程式碼
鍵空間和使用者所見的資料庫是直接對應的:
鍵空間的鍵也就是資料庫的鍵,每個鍵都是一個字串物件。鍵空間的值也就是資料庫的值,每個值可以使字串物件、列表物件、雜湊表物件、集合物件和有序集合物件中的任意一種Redis物件。對鍵值對進行增刪改查詢操作,就是在鍵空間字典裡面,透過key來進行操作,找到key指向的value,進行對應的增刪改查操作。
當使用Redis命令對資料庫進行讀寫時,伺服器不僅會對鍵空間執行指定的讀寫操作,還會執行一些額外的維護操作,包括:
讀取一個鍵(讀寫操作都需要對鍵進行讀取)後,伺服器會根據鍵是否存在來更新伺服器的鍵空間命中次數(hit)或者鍵空間不命中(miss)次數。在讀取一個鍵後,伺服器會更細鍵的LRU時間,這個值可以計算鍵的閒置時間。如果伺服器在讀取一個鍵時發現該鍵已經過期,那麼伺服器會先刪除這個過期鍵,然後才執行餘下的其他操作。如果有客戶端使用WATCH命令監視某個鍵,那麼伺服器在對被監視的鍵進行修改之後,會將這個鍵標記為髒(dirty),從而讓事務程式注意到這個程式已經被修改。伺服器每次修改一個鍵之後,都會對髒鍵計數器的值增1,這個計數器會觸發伺服器的持久化以及複製操作。如果伺服器開啟了資料庫通知功能,那麼在對鍵進行修改之後,伺服器將按配置傳送相應的資料庫通知。設定鍵的生存時間或過期時間透過EXPIRE命令或者PEXPIRE命令,客戶端可以以秒或者毫秒精度為資料庫中的某個鍵設定生存時間(TTL, Time To Live),在經過指定的秒數或者毫秒數之後,伺服器就會自動刪除生存時間為0的鍵。
EXPIRE key seconds #設定key的過期時間,超過時間後,將會自動刪除該keyEXPIREAT key timestamp #和EXPIRE類似,都用於為key設定生存時間。不同在於EXPIREAT命令接受的時間引數是UNIX時間戳Unix-timestampPERSIST key #移除給定key的生存時間,將這個key從帶生存時間key轉換成不帶生存時間、永不過期的keyPEXPIRE key milliseconds #和EXPIRE命令的作用類似,但是它以毫秒為單位設定key的生存時間PEXPIREAT key milliseconds-timestamp #和EXPIREAT命令類似,但它以毫秒為單位設定key的過期unix時間戳PTTL key #類似於TTL命令,但它以毫秒為單位返回key的剩餘生存時間TTL key #返回key剩餘的過期時間複製程式碼
雖然有多種不同單位和不同形式的設定過期時間命令,但最終都是用過PEXPIREAT命令實現的。
redisDb結構的expires字典儲存了資料庫中所有鍵的過期時間,我們稱這個字典為過期字典。
定時刪除:主動刪除,在設定鍵的過期時間的同時,建立一個定時器,讓定時器在鍵過期的時間來臨時,立即執行對鍵的刪除操作。優劣:對記憶體友好,可以保證過期鍵被儘可能快的刪除釋放過期鍵佔用的記憶體空間;對CPU不友好,若有大量請求在等待伺服器處理,CPU時間用在刪除和當前任務無關的過期鍵上,影響伺服器的響應時間和吞吐量。惰性刪除:被動刪除,放任鍵過期不管,但每次從鍵空間獲取鍵時,都檢查取得的鍵是否過期,如果過期的話,就刪除該鍵;如果沒過期,就返回該鍵。優劣:對CPU友好,取鍵時才過期檢查;對記憶體不友好庫中可能存在大量沒被訪問到的過期鍵,它們可能永遠不會被刪除。定期刪除:主動刪除,每隔一段時間,程式就對資料庫進行一次檢查,刪除裡面的過期鍵。至於要刪除多少過期鍵,以及要檢查多少個數據庫,則由演算法決定。是前兩種刪除策略的折中,難點:刪除太頻繁執行時長過長,定期刪除就會退化成定時刪除策略;刪除執行過少或執行時長過短,定期刪除又會和惰性刪除一樣。Redis的過期鍵刪除策略:Redis伺服器實際使用的是惰性刪除和定期刪除兩種策略。
函式每次執行時,都從一定數量的資料庫中取出一定數量的隨機鍵進行檢查,並刪除其中的過期鍵。全域性遍歷current_db會記錄當前activeExpireCycle函式檢查的進度,並在下一次activeExpireCycle函式呼叫時,接著上一次的進度進行處理。隨著activeExpireCycle函式的不斷執行,伺服器中的所有資料庫都會被檢查一遍,這時函式將current_db變數重置為0,然後再次開始新一輪的檢查工作。記憶體淘汰策略:
volatile-lru:使用lru演算法(Least Recently Used,最近最久未使用),從已設定過期時間的資料集中挑選最近最少使用的淘汰;volatile-ttl:從已設定過期時間的資料集中挑選將要過期的資料淘汰;volatile-random:從已設定過期時間的資料集中任意選擇資料淘汰;allkeys-lru:使用lru演算法,從資料集中選擇最近最少使用的淘汰;allkeys-random:從資料集中選擇任意資料淘汰;noenviction(驅逐):禁止淘汰資料;當記憶體不足以寫入新資料時,直接報異常,Redis只響應讀操作。預設使用noenviction記憶體淘汰策略。
AOF、RDB和複製功能對過期鍵的處理生成RDB檔案:在執行SAVE命令或者BGSAVE命令建立一個新的RDB檔案時,程式會對資料庫中的鍵進行檢查,已過期的鍵不會被儲存到新建立的RDB檔案中。因此,資料庫中包含過期鍵不會對生成新的RDB檔案造成影響。
載入RDB檔案:在啟動Redis伺服器時,如果伺服器開啟了RDB功能,那麼伺服器將對RDB檔案進行載入:
如果伺服器以主伺服器模式執行,那麼在載入RDB檔案時,程式會對檔案中儲存的鍵進行檢查,未過期的鍵會被載入到資料庫,而過期鍵則會被忽略,所以過期鍵對載入RDB檔案的主伺服器不會造成影響。如果伺服器以從伺服器模式執行,那麼在載入RDB檔案時,檔案中儲存的所有鍵,不論是否過期,都會被載入到資料庫中。不過,因為主從伺服器在進行資料同步的時候,從伺服器的資料庫就會被清空,所以一般來講,過期鍵對載入RDB檔案的從伺服器也不會造成影響。AOF檔案寫入:當伺服器以AOF持久化模式執行時,如果資料庫中的某個鍵已經過期,但它還沒有被惰性刪除或者定期刪除,那麼AOF檔案不會因為這個過期鍵而產生任何影響。當過期鍵被惰性刪除或者定時刪除之後,程式會向AOF檔案追加一條DEL命令,來顯式地記錄該鍵已被刪除。
AOF重寫:和生成RDB檔案類似,在執行AOF重寫的過程中,程式會對資料庫中的鍵進行檢查,已經過期的鍵不會被儲存到重寫後的AOF檔案中。因此,資料庫中包含過期鍵不會對AOF重寫造成影響。
主伺服器在刪除一個過期鍵時,會顯式地向所有從伺服器傳送一個DEL命令,告知從伺服器刪除這個過期鍵。從伺服器在執行客戶端傳送的讀命令時,即使碰到過期鍵也不會將過期鍵刪除,而是繼續像處理未過期的鍵一樣來處理過期鍵。從伺服器只有在接到主伺服器傳送過來的DEL命令之後,才會刪除過期鍵。通知由主伺服器來控制從伺服器統一地刪除過期鍵,可以保證主從伺服器資料的一致性,也正是因為這個原因,當一個過期鍵仍然存在於主伺服器的資料庫時,這個過期鍵在從伺服器裡的複製品也會繼續存在。
資料庫通知資料庫通知是Redis2.8版本新增加的功能,這個功能可以讓客戶端透過定於給定的頻道或者模式,來獲知資料庫中鍵的變化,以及資料庫中的命令執行情況。
鍵通知由兩種:分別是鍵空間通知,即某個鍵執行了什麼命令;和鍵事件通知,即某個命令被什麼鍵執行了。
這部分功能在後續的釋出訂閱功能中會細講,這裡簡略提及。
傳送通知的功能是由notify.c/notifyKeyspaceEvent實現的,步驟如下:
server.notify_keyspace_events屬性就是伺服器配置notify_keyspace_events選項所設定的值,如果給定的通知型別type不是伺服器允許傳送的通知型別,那麼函式會直接返回,不做任何操作。如果給定的通知是伺服器允許傳送的通知,那麼下一步函式會檢測伺服器是否允許傳送鍵空間通知,如果允許的話,程式就會構建併發送事件通知。最後,函式檢測伺服器是否允許傳送鍵事件通知,如果允許的話,程式就會構建併發送事件通知。RDB持久化因為Redis是記憶體資料庫,它將自己的資料庫狀態儲存在記憶體裡面,所以如果不想辦法將儲存在記憶體的資料庫狀態儲存到磁盤裡面,那麼一旦伺服器程序退出,伺服器中的狀態也會消失不見。
為了解決這個問題,Redis提供了RDB持久化功能,這個功能可以將Redis在記憶體的資料庫狀態儲存到磁盤裡面,避免資料意外丟失。
將資料庫狀態儲存為RDB檔案:
用RDB檔案來還原資料庫狀態:
RDB檔案的建立與載入可以透過SAVE命令或者BGSAVE命令來生成RDB檔案。
SAVE命令會阻塞Redis伺服器程序,直到RDB檔案建立完畢為止,在伺服器程序阻塞期間,伺服器不能處理任何命令請求;而BGSAVE命令會派生出一個子程序,然後由子程序負責建立RDB檔案,伺服器程序繼續處理命令請求。
RDB檔案的載入工作是在伺服器啟動時自動執行的,所以Redis並沒有專門用於載入RDB檔案的命令,只要Redis伺服器在啟動時自動檢測到RDB檔案存在,它就會自動載入RDB檔案。另外,因為AOF檔案的更新頻率通常比RDB檔案更新頻率高,所以如果伺服器開啟了AOF持久化功能,那麼伺服器會優先使用AOF檔案來還原資料庫狀態。
SAVE命令執行過程中,Redis伺服器會被阻塞,客戶端傳送的所有命令請求都會被拒絕。
BGSAVE命令執行過程中,Redis伺服器仍然可以繼續處理客戶端的命令請求,而SAVE、BGSAVE、BGREWRITEAOF命令都會被拒絕。
伺服器在載入RDB檔案期間,會一直處於阻塞狀態,直到載入工作完成為止。
自動間隔性儲存Redis允許使用者透過設定伺服器配置的save選項,讓伺服器每隔一段時間自動執行一次BGSAVE命令,使用者可以透過save選項設定多個儲存條件,但只要其中任意一個條件被滿足,伺服器就會執行BGSAVE操作。
save time times,即在多少秒內,對資料庫進行了至少多少次操作,就會觸發BGSAVE命令執行操作。
save 900 1save 300 10save 60 10000複製程式碼
這裡的實現是透過redisServer結構的saveparams屬性來實現的:
struct saveparam { time_t seconds;//秒數 int changes;//修改數};複製程式碼
除了saveparams陣列之外,伺服器狀態還維持著一個dirty計數器,以及一個lastsave屬性:
dirty計數器記錄距離上一次執行SAVE命令或者BGSAVE命令之後,伺服器對資料庫狀態進行了多少次修改(寫入、刪除、更新等)操作。lastsave屬性是一個unix時間戳,記錄了伺服器上一次成功執行SAVE命令或者BGSAVE命令的時間。在有了saveparams陣列和dirty+lastsave屬性組合的前提下,Redis的伺服器週期性操作函式serverCron預設每隔100毫米就會執行一次,該函式用於對正在執行的伺服器進行維護,它的其中一項工作就是檢查save選項所設定的儲存條件是否滿足,如果滿足的話,就會執行BGSAVE命令。
RDB檔案結構RDB檔案的最開頭是REDIS部分,這個部分長度為5位元組,儲存著"REDIS"五個字元,類似於java檔案的CAFEBABE魔數。透過這五個字元,程式可以在載入檔案時,快速檢查所載入的檔案是否是RDB檔案。
db_version長度為4個位元組,它的值是一個字串表示的整數,這個整數記錄了RDB檔案的版本號。這篇文章只介紹第六版RDB檔案的結構。
databases部分包含著零個到任意多個數據庫,以及各個資料庫中的鍵值對資料。
EOF常量的長度為1位元組,這個常量標誌著RDB檔案正文內容的結束,當讀入成功遇到這個值的時候,它就知道資料庫的所有鍵值對都已經載入完畢了。
check_sum是一個8位元組長的無符號整數,儲存著一個校驗和,這個校驗和是透過對前面四部分內容進行計算得出的。伺服器在載入RDB檔案時,會將載入資料所計算出的校驗和與check_sum所記錄的校驗和進行對比,一次來檢查RDB檔案是否有出錯或者損壞的情況出現。
databases部分一個RDB檔案的databases部分可以儲存任意多個非空資料庫,每個非空資料庫在RDB檔案中都可以儲存為SELECTDB、db_number、key_value_pairs三個部分。
SELECTDB:長度為1位元組,當讀入程式遇到這個值時,它知道接下來要讀入的將是一個數據庫號碼。
db_number:儲存著一個數據庫號碼,根據號碼的大小不同,這個部分的長度可以使1位元組、2位元組或者5位元組。
key_value_pairs:儲存了資料庫中的所有鍵值對資料,如果鍵值對帶有過期時間,那麼過期時間也會和鍵值對儲存在一起。
RDB檔案中的資料庫結構示例:
key_value_pairs部分RDB檔案中的每個key_value_pairs部分都儲存了一個或以上數量的鍵值對,如果鍵值對帶有過期時間的話,那麼鍵值對的過期時間也會被儲存在內。
不帶過期時間的鍵值對在RDB檔案中由TYPE、key、value三部分組成。
TYPE記錄了value的型別,長度為1位元組,值可以是以下常量的其中一個:
REDIS_RDB_TYPE_STRINGREDIS_RDB_TYPE_LISTREDIS_RDB_TYPE_SETREDIS_RDB_TYPE_ZSETREDIS_RDB_TYPE_HASHREDIS_RDB_TYPE_LIST_ZIPLISTREDIS_RDB_TYPE_SET_INTSETREDIS_RDB_TYPE_ZSET_ZIPLISTREDIS_RDB_TYPE_HASH_ZIPLIST以上列出的每個TYPE常量都代表了一種物件型別或者底層編碼,這部分已經在上一篇文章內詳細學習過了。
key和value分別儲存了鍵值對的鍵物件和值物件。其中key總是一個字串物件,而value物件會有不同的結構儲存方式。
帶有過期時間的鍵值對在RDB檔案中的結構如下圖:
EXPIRETIME_AT常量的長度為1自己誒,它告知讀入程式,接下來要讀入的將是一個以毫秒為單位的過期時間。ms是一個8位元組長的帶符號整數,記錄著一個以毫秒為單位的UNIX時間戳,這個時間戳就是鍵值對的過期時間。後面value部分就不詳細記錄了,具體內容可以參考書上內容,只需要記字串大於20位元組會被壓縮後再儲存,其他的物件的儲存結構都類似於壓縮列表的儲存方式,緊密挨在一起。
AOF持久化除了RDB持久化功能外,Redis還提供了AOF(Append Only File)持久化功能,與RDB持久化透過儲存資料庫中的鍵值對狀態來記錄資料庫狀態不同,AOF持久化是透過儲存Redis伺服器所執行的寫命令來記錄資料庫狀態的。
AOF持久化的實現AOF持久化功能的實現可以分為命令追加、檔案寫入和檔案同步三個步驟。
命令追加:
當AOF持久化功能處於開啟狀態時,伺服器在執行完一個寫命令之後,會以協議格式將被執行的寫命令追加到伺服器狀態的aof_buf緩衝區的末尾。
舉個例子,客戶端向伺服器傳送redis> set key value這個命令,然後開啟aof檔案:
可以看到,命令被追加到aof檔案中去了。
AOF檔案的寫入和同步:
前面說過,客戶端發出的寫命令是被寫入到緩衝區,那是如何落盤到磁盤裡的?這就要提AOF檔案的寫入和同步功能了。
Redis的伺服器程序就是一個時間迴圈,這個迴圈中的檔案事件負責接收客戶端的命令請求,以及向客戶端傳送命令回覆,而時間事件則負責像serverCron函式這樣需要定時執行的函式。
因為伺服器在處理檔案事件時可能會執行寫命令,使得一些內容被追加到aof_buf緩衝區裡面,所以伺服器每次結束一個事件迴圈之前,它都會呼叫flushAppendOnlyFile函式,考慮是否需要將aof_buf緩衝區中的內容寫入和儲存到AOF檔案裡面。
flushAppendOnlyFile函式的行為由伺服器配置的appendfsync選項的值來決定,各個不同值產生的行為如下:
AOF檔案的載入與資料還原:
因為AOF檔案裡面包含了重建資料庫狀態所需的所有寫命令,所以伺服器只要讀入並重新執行一遍AOF檔案裡面儲存的寫命令,就可以還原伺服器關閉之前的資料庫狀態。
Redis讀取AOF檔案並還原資料庫狀態的詳細步驟如下:
建立一個不帶網路連線的偽客戶端,因為Redis的命令只能在客戶端上下文中執行,而載入AOF檔案時所使用的命令直接來源於AOF檔案而不是網路連線,所以伺服器使用一個沒有網路連線的偽客戶端來執行AOF檔案儲存的寫命令,偽客戶端執行命令的效果和帶網路連線的客戶端執行命令的效果完全一樣。從AOF檔案中分析並讀出一條寫命令。使用偽客戶端執行被讀出的寫命令。一直執行步驟2和步驟3,直到AOF檔案中的所有寫命令都被處理完畢為止。完整過程如下圖:
AOF重寫因為AOF持久化是透過儲存被執行的寫命令來記錄資料庫狀態的,所以隨著伺服器執行時間的流逝,AOF檔案中的內容會越來越多,檔案的體積也會越來越大,如果不加以控制的話,體積過大的AOF檔案很可能對Redis伺服器、甚至整個宿主計算機造成影響,並且AOF檔案的體積越大,使用AOF檔案來進行資料還原所需的時間越多。
為了解決AOF檔案體積膨脹的問題,Redis提供了AOF檔案重寫功能。透過該功能,Redis伺服器可以建立一個新的AOF檔案來替代現有的AOF檔案,新舊兩個AOF檔案儲存的資料庫狀態相同,但新的AOF檔案不會包含任何浪費空間的冗餘命令,所以新的AOF檔案的體積通常會比舊AOF檔案的體積要小得多。
AOF檔案重寫的實現:
雖然Redis將生成新AOF檔案替換舊AOF檔案的功能命名為“AOF檔案重寫”,但實際上,AOF檔案重寫並不需要對現有的AOF檔案進行任何讀取、分析或者寫入操作,這個功能是透過讀取伺服器當前的資料庫狀態來實現的。即新生成的AOF檔案不需要考慮舊AOF檔案的資料,直接透過讀取現有的Redis伺服器資料庫狀態,來寫入命令,從而實現去除浪費空間的冗餘命令。
AOF後臺重寫:
上面介紹的AOF重寫程式aof_rewrite函式可以很好地完成建立一個新AOF檔案的任務,但是,因為這個函式會進行大量的寫入操作,所以呼叫這個函式的執行緒將被長時間阻塞,因為Redis伺服器使用單個執行緒來處理命令請求,所以如果由伺服器直接呼叫aof_rewrite函式的話,那麼在重寫AOF檔案期間,伺服器將無法處理客戶端發來的命令請求。
很明顯,作為一個輔佐性的維護手段,Redis不希望AOF重寫造成伺服器無法處理請求,所以Redis決定將AOF重寫程式放到子程序裡執行,這樣做可以同時達到兩個目的:
子程序進行AOF重寫期間,伺服器程序可以繼續處理命令請求。子程序帶有伺服器程序的資料副本,使用子程序而不是執行緒,可以避免使用鎖的情況,保證資料的安全性。不過,使用子程序也有一個問題需要解決,因為子程序在進行AOF重寫期間,伺服器程序還需要繼續處理命令請求,而新的命令可能會對現有的資料庫狀態進行修改,從而使得伺服器當前的資料庫狀態和重寫後的AOF檔案所儲存的資料庫狀態不一致。
為了解決這種資料不一致問題,Redis伺服器設定了一個AOF重寫緩衝區,這個緩衝區在伺服器建立子程序後開始使用,當Redis伺服器執行完一個寫命令之後,它會同時將這個寫命令傳送給AOF緩衝區和AOF重寫緩衝區。
這樣一來,可以保證:
AOF緩衝區的內容會被定時寫入和同步到AOF檔案,對現有AOF檔案的處理工作會如常進行。從建立子程序開始,伺服器執行的所有寫命令都會被記錄到AOF重寫緩衝區裡面。當子程序完成AOF檔案重寫工作之後,它會向父程序發出一個訊號,父程序在接到該訊號之後,會呼叫一個訊號處理函式,並執行以下工作:
將AOF重寫緩衝區的所有內容寫入到新AOF檔案中,這時新AOF檔案所儲存的資料庫狀態將和伺服器當前的資料庫狀態一致。對新的AOF檔案進行改名,原子地覆蓋現有的AOF檔案,完成新舊兩個AOF檔案的替換。這個訊號處理函式執行完畢後,父程序就可以繼續像往常一樣接收命令請求了。
在整個AOF後臺重寫過程中,只有訊號處理函式執行時會對伺服器程序造成阻塞,在其他時候,AOF後臺重寫都不會阻塞父程序,這將AOF重寫對伺服器效能造成的影響降到了最低。
以下舉個例子
以上就是AOF後臺重寫,即BGREWRITEAOF命令的實現原理。
事件Redis伺服器是一個事件驅動程式,伺服器需要處理以下兩類事件:
檔案事件:Redis伺服器透過socket與客戶端(或者其他Redis伺服器)進行連線,而檔案事件就是伺服器對socket操作的抽象。伺服器與客戶端的通訊會產生相應的檔案事件,而伺服器則透過監聽並處理這些事件來完成一系列網路通訊操作。
時間事件:Redis伺服器的一些操作(比如serverCron函式)需要在給定的時間點進行,而時間事件就是伺服器對這類定時操作的抽象。
檔案事件Redis基於Reactor模式開發了自己的網路事件處理器:這個處理器被稱為檔案事件處理器:
檔案事件處理器使用IO多路複用程式來同時監聽多個socket,並根據socket目前執行的任務來為socket關聯不同的事件處理器。當被監聽的socket準備好執行連線應答、讀取、寫入、關閉等操作時,與操作相對應的檔案事件就會產生,這時檔案事件處理器就會呼叫socket之前關聯好的事件處理器來處理這些事件。雖然檔案事件處理器以單執行緒方式執行,但透過IO多路複用程式來監聽多個socket,檔案事件處理器既實現了高效能的網路通訊模型,又可以很好地與Redis伺服器中其他同樣以單執行緒方式執行的模組進行對接,這保持了Redis內部單執行緒設計的簡單性。
檔案事件處理器的構成:
檔案事件處理器的四個組成部分,分別是socket、IO多路複用程式、檔案事件分派器和事件處理器。
檔案事件是對socket操作的抽象,每當一個socket準備好執行連線應答、寫入、讀取、關閉等操作時,就會產生一個檔案事件。因為一個伺服器通常會連線多個socket,所以多個檔案事件可能會併發地出現。
IO多路複用程式負責監聽多個socket,並向檔案事件分派器傳送那些產生了事件的socket。
儘管多個檔案事件可能會併發地出現,但IO多路複用程式總是會將所有產生事件的socket都放在一個佇列裡面,然後透過這個佇列,以有序、同步、每次一個socket的方式向檔案事件分派器傳送socket。當一個socket產生的事件被處理完畢之後,IO多路複用程式才會繼續向檔案事件分派器傳送下一個socket。
檔案事件分派器接受IO多路複用程式傳來的socket,並根據socket產生的事件的型別,呼叫相應的事件處理器。
伺服器會執行不同人物的socket關聯不同的事件處理器,這些處理器是一個個函式,它們定義了某個事件發生時,伺服器應該執行的操作。
IO多路複用程式的實現:
Redis的IO多路複用程式的所有功能都是透過包裝常見的select、epoll、evport、kqueue這些IO多路複用函式庫來實現的。因為Redis為每個IO多路複用函式庫都實現了相同的API,所以IO多路複用程式的底層實現是可以互換的。
事件的型別:
IO多路複用程式下可以監聽多個socket的ae.h/AR_READABLE事件和ae.h/AE_WRITABLE事件,這兩類事件和socket操作之間的對應關係如下:
當socket變得可讀時(客戶端對socket執行write操作,或者執行close操作),或者有新的可應答(acceptable)socket出現時(客戶端對伺服器的監聽socket執行connect操作),socket產生AE_READABLE事件。當socket變得可寫時(客戶端對socket執行read操作),socket產生AE_WRITABLE事件。IO多路複用程式允許伺服器同時監聽socket的ae.h/AR_READABLE事件和ae.h/AE_WRITABLE事件,如果一個socket同時產生了這兩個事件,那麼檔案事件分派器會優先處理ae.h/AR_READABLE事件,等到ae.h/AR_READABLE事件處理完之後,才處理ae.h/AE_WRITABLE事件。
這也就是說,如果一個socket可讀又可寫時的話,那麼伺服器將先讀後寫。
檔案事件的處理器:
Redis為檔案事件編寫了多個處理器,這些處理器分別用於實現不同的網路通訊需求,在這些事件處理器裡面,伺服器最常用的要數與客戶端進行通訊的連線應答處理器、命令請求處理器和命令回覆處理器。
連線應答處理器:在networking.c/acceptTcpHandler函式中實現,這個處理器用於對接伺服器監聽socket的客戶端進行應答,具體實現為sys/socket.h/accept函式的包裝。當Redis伺服器進行初始化的時候,程式會將這個連線應答處理器和伺服器監聽socket的AE_READABLE事件關聯起來,當有客戶端用sys/socket.h/connect函式連結伺服器監聽socket時,socket就會產生AE_READABLE事件,引發連線應答處理器執行,並執行相應的socket應答操作。命令請求處理器:在networking.c/readQueryFromClient函式中實現,是Redis的命令請求處理器,這個處理器負責從scoket中讀入客戶端傳送的命令請求內容,具體實現為unistd.h/read函式的包裝。當一個客戶端透過連線應答處理器成功連線到伺服器之後,伺服器會將客戶端的socket的AE_READABLE事件和命令請求處理器關聯起來,當客戶端向伺服器傳送命令請求的時候,socket就會產生AE_READABLE事件,引發命令請求處理器執行,並執行相應的socket讀入操作。在客戶端連線伺服器的整個過程中,伺服器都會一直為客戶端socket的AE_READABLE事件關聯命令請求處理器。命令回覆處理器:在networking.c/sendReplyToClient函式中實現,是Redis的命令回覆處理器,這個處理器負責將伺服器執行命令後得到的命令回覆透過socket返回給客戶的,具體實現為unistd.h/write函式的包裝。當伺服器有命令回覆需要傳送給客戶端時,伺服器會將客戶端socket的AE_WRITABLE事件和命令回覆處理器關聯起來,當客戶端準備好接收伺服器傳回的命令回覆時,就會產生AE_WRITABLE事件,引發命令回覆處理器執行,並執行相應的socket寫入操作,當命令回覆傳送完畢之後,伺服器就會解除命令回覆處理器與客戶端socket的AE_WRITABLE事件之間的關聯。時間事件Redis的時間事件分為以下兩類:
定時事件:讓一段程式在指定的時間之後執行一次。週期性事件:讓一段程式每隔指定時間就執行一次。一個時間事件主要由以下三個屬性組成:
id:伺服器為時間事件建立的全域性唯一ID,ID號按從小到大的順序遞增,新事件的ID號比舊事件的ID號要大。when:毫秒精度的UNIX時間戳,記錄了時間事件的到達時間。timeProc:時間事件處理器,一個函式,當時間事件到達時,伺服器就會呼叫相應的處理器來處理事件。一個時間事件是定時事件還是週期性事件取決於時間事件處理器的返回值:
如果事件處理器返回ae.h/AE_NOMORE,那麼這個事件定義為定時事件:該事件在達到一次之後就會被刪除,之後不再道道。如果事件處理器返回一個非AE_NOMORE的整數值,那麼這個事件為週期性事件:當一個時間事件到達之後,伺服器會根據事件處理器返回的值,對時間事件的when屬性進行更新,讓這個事件在一段時間之後再次到達,並以這種方式一直更新並執行下去。目前版本的Redis只使用週期性事件,而沒有使用定時事件。
實現:
伺服器將所有時間事件都放在一個無序連結串列中,每當時間事件執行器執行時,它就遍歷整個連結串列,查詢所有已到達的時間事件,並呼叫相應的事件處理器。
如圖所示:
無序連結串列指的是不按照時間事件的到達時間when進行排序。
時間事件應用例項:serverCron函式:
持續執行的Redis伺服器需要定期對自身的資源和狀態進行檢查和調整,從而確保伺服器可以長期、穩定地執行,這些定期操作由redis.c/serverCron函式負責執行,它的主要工作包括:
更新伺服器的各類統計資訊,比如時間、記憶體佔用、資料庫佔用情況等。清理資料庫中的過期鍵值對。關閉和清理連線失效的客戶端。嘗試進行AOF或RDB持久化操作。如果伺服器是主伺服器,那麼對從伺服器進行定期同步。如果處於叢集模式,對叢集進行定期同步和連線測試。Redis伺服器以週期性事件的方式來執行serverCron函式,在伺服器執行期間,每隔一段時間,serverCron就會執行一次,直到伺服器關閉為止。
事件的排程與執行因為伺服器中同時存在檔案事件和時間事件兩種事件型別,所以伺服器必須對這兩種事件進行排程,決定何時應該處理檔案事件,何時2又該處理時間事件,以及花多少時間來處理它們等等。
事件的排程和執行規則:
aeApiPoll函式的最大阻塞時間由到達時間最接近當前時間的時間事件決定,這個方法既可以避免伺服器對時間事件進行頻繁的輪詢(忙等待),也可以確保aeApiPoll函式不會阻塞過長時間。因為檔案事件是隨機出現的,如果等待並處理完一次檔案事件之後,仍未有任何時間事件到達,那麼伺服器將再次等待並處理檔案事件。隨著檔案事件的不斷執行,時間會逐漸向時間事件所設定的到達時間逼近,並最終來到到達時間,這時伺服器就可以開始處理到達的時間事件了。對檔案事件和時間事件的處理都是同步、有序、原子地執行的,伺服器不會中途中斷事件處理,也不會對事件進行搶佔,因此,不管是檔案事件的處理器,還是時間事件的處理器,它們都會盡可能減少程式的阻塞時間,並在有需要時主動讓出執行權,從而降低造成事件飢餓的可能性。因為時間事件在檔案事件之後執行,並且事件之間不會出現搶佔,所以時間事件的實際處理時間,通常會比時間事件設定的的到達時間稍晚一些。總結在這篇文章中,學習了單機資料庫的原理,包括鍵空間、過期鍵的實現、過期鍵刪除策略、最大記憶體淘汰策略,然後是持久化,持久化分為RDB和AOF,AOF持久化還有AOF重寫,然後是事件,事件分為檔案事件和時間事件,檔案事件實際上是客戶端和伺服器之間透過socket連線,伺服器應答、處理、回覆客戶端的一系列事件,而時間事件是定時的或者週期性的到達某個時間點Redis需要進行的事件。
但是單機資料庫面臨著四個無法解決的問題:讀壓力、寫壓力、資料備份、故障自愈,下一篇文章將學習多機資料庫及其原理,來看看不同的玩法是透過何種方式來如何解決單機Redis無法解決的問題。