Redis的內部運作機制


  本文將分五個部分來分析和總結Redis的內部機制,分別是:Redis數據庫、Redis客戶端、Redis事件、Redis服務器的初始化步驟、Redis命令的執行過程。

  首先介紹一下Redis服務器的狀態結構。Redis使用一個類型為“redisServer”的數據結構來保存整個Redis服務器的狀態(每個屬性按照即將講解的順序進行排序):

struct redisServer {
int dbnum;//服務器的數據庫數量,值由服務器配置的“databases”選項決定,默認為16
redisDb *db;//數組,保存着服務器中的所有數據庫

list *clients;//一個鏈表,保存了所有客戶端狀態,每個鏈表元素都是“redisClient”結構

time_t unixtime;//保存秒級精度的系統當前UNIX時間戳,減少獲取系統當前時間的系統調用次數,100毫秒更新一次
long long mstime;//保存毫秒級精度的系統當前UNIX時間戳
unsigned lruclock;//默認每10秒更新一次,用於計算數據庫鍵的空轉時長,數據庫鍵的空轉時長 = 服務器的“lruclock”屬性值 - 數據庫鍵值對象的“lru”屬性值

long long ops_sec_last_sample_time;//上一次進行服務器每秒執行命令數量抽樣的時間
long long ops_sec_last_sample_ops;//上一次進行服務器每秒執行命令數量抽樣時,服務器已執行命令的數量
long long ops_sec_samples[REDIS_OPS_SEC_SAMPLE];//環形數組,每個元素記錄一次服務器每秒執行命令數量抽樣結果,估算服務器在最近一秒鍾處理的命令請求數量(數組長度默認為16,100毫秒更新一次)
int ops_sec_idx;//ops_sec_samples數組的索引值,每次抽樣后值增1,等於16時重置為0

size_t stat_peak_memory;//已使用內存峰值

int shutdown_asap;//關閉服務器的標識,1表示關閉,0不關閉

pid_t rdb_child_pid;//記錄執行BGSAVE命令的子進程的ID,-1表示服務器沒有正在執行BGSAVE
pid_t aof_child_pid;//記錄執行BGREWRITEAOF命令的子進程的ID,-1表示服務器沒有正在執行BGREWRITEAOF
int aof_rewrite_scheduled;//1表示有BGREWRITEAOF命令被延遲了(服務器執行BGSAVE期間收到的BGREWRITEAOF會被延遲到BGSAVE執行完成之后執行)
struct saveparam *saveparams;//記錄了自動保存條件的數組(執行BGSAVE的條件)
long long dirty;//修改計數器(上一次執行BGSAVE之后已經產生了多少修改)
time_t lastsave;//上一次執行自動保存操作(BGSAVE)的時間
sds aof_buf;//AOF緩沖區

int cronloops;//serverCron函數的運行次數計數器

lua;//用於執行Lua腳本的Lua環境
redisClient *lua_client;//Lua腳本的偽客戶端,在服務器運行的整個生命周期一直存在,直至服務器關閉才會關閉
dict *lua_scripts;//字典,記錄所有載入的Lua腳本,鍵為某個Lua腳本的SHA1校驗和,值為對應的Lua腳本
dict *repl_scriptcache_dict;//字典,記錄已經傳播給所有從服務器的所有Lua腳本,鍵為腳本的SHA1校驗和,值為NULL,用於EVALSHA1命令的復制

long long slowlog_entry_id;//下一條慢查詢日志的ID
list *slowlog;//保存了所有慢查詢日志的鏈表
long long slowlog_log_slower_than;//服務器配置“slowlog-log-slower-than”選項的值,表示查詢慢於多少微秒便記錄慢查詢日志
unsigned long slowlog_max_len;//服務器配置“slowlog-max-len”選項的值,表示服務器最多保存多少條慢查詢日志記錄,若超出,最久的記錄會被覆蓋

monitors;//鏈表,監視器客戶端列表

dict *pubsub_channels;//字典,保存所有頻道的訂閱關系,鍵為某個被訂閱的頻道,值為鏈表,記錄了所有訂閱這個頻道的客戶端
list *pubsub_patterns;//鏈表,保存所有模式的訂閱關系,每個鏈表節點都包含了訂閱的客戶端和被訂閱的模式
};

 

Redis數據庫
  在Redis服務器狀態結構中,“dbnum”屬性記錄了服務器的數據庫數量,它的值可以通過服務器配置的“databases”選項決定,默認為16。“db”屬性是一個數組,保存保存着服務器中的所有數據庫,其中每一個數據庫都對應一個“redisDb”數據結構:

struct redisDb {
dict *dict;//數據庫鍵空間字典,保存數據庫中所有的鍵值對
dict *expires;//過期字典,保存數據庫中所有鍵的過期時間
dict *watched_keys;//字典,正在被WATCH命令監視的鍵
};

  

數據庫鍵空間
  Redis數據庫結構的“dict”屬性是Redis數據庫的鍵空間,底層由字典實現。所有在數據庫上的增刪改查,實際上都是通過對鍵空間字典進行相應操作來實現的。除此之外,還需要進行一些額外的維護操作,主要有如下操作內容:
(1)在讀取一個鍵之后,服務器會根據鍵是否存在來更新服務器的鍵空間命中次數或不命中次數(這兩個值可以在'info stats'命令返回中的'keyspace_hits'屬性和'keyspace_misses'屬性中查看)。
(2)在讀取一個鍵之后,服務器會更新鍵的LRU屬性值(最后一次使用時間,使用'object idletime'命令可以查看鍵的閑置時間)。
(3)在讀取一個鍵時,若發現該鍵已過期,則刪除這個鍵。
(4)如果有客戶端使用'watch'命令監視了某個鍵,服務器在對被監視的鍵進行修改之后,會將這個鍵標記為臟,從而讓事務程序注意到這個鍵已經被修改過了。
(5)服務器每次修改一個鍵之后,都會對臟鍵計數器(即Redis服務器狀態的'dirty'屬性)的值加一,這個計數器會觸發服務器的持久化以及復制操作。
(6)如果服務器開啟了數據庫通知功能,在對鍵進行修改之后,服務器將按配置發送相應的數據庫通知。

 

過期字典
  Redis數據庫結構的“expires”屬性保存了Redis數據庫所有擁有過期時間的鍵以及它們對應的過期時間,底層同樣由字典實現。數據庫鍵過期時間的設置和刪除,實際上都是對過期字典的操作。其中,字典的鍵是一個個指針,分別指向鍵空間字典中的一個個鍵對象(共享對象,節省內存空間);字典的值則是一個個long long類型的整數表示的毫秒精度的UNIX時間戳,保存數據庫鍵的過期時間。
1. 鍵過期時間設置
expire:以秒為單位,設置Redis鍵的生存時間。
pexpire:以毫秒為單位,設置Redis鍵的生存時間。
expireat:以秒為單位,設置Redis鍵的過期時間。
pexpireat:以毫秒為單位,設置Redis鍵的過期時間。
注:實際上'expire'、'pexpire'、'expireat'命令最后都會轉換為'pexpireat'命令來執行。
2. 鍵過期時間查看
ttl:以秒為單位,返回鍵的剩余生存時間。
pttl:以毫秒為單位,返回鍵的剩余生存時間。
3. 鍵過期判定
  檢查當前Unix時間戳是否大於鍵的過期時間,是則過期,否則不過期。
4. 過期鍵的理論刪除策略
(1)定時刪除:
  設置一個鍵過期時間的同時,創建一個定時器。每個帶有過期時間的鍵都對應着一個定時器。
  這種策略對內存是最友好的,但對CPU時間是最不友好的。創建一個定時器需要用到Redis服務器中的時間事件,而當前時間事件的實現方式為無序鏈表,查找一個事件的時間復雜度為O(N),並不能高效地處理大量時間事件。
(2)惰性刪除:
  訪問一個鍵的時候再檢測該鍵是否過期,是則刪除之。
  這種策略對CPU時間是最友好的,但對內存是最不友好的。沒被訪問到的過期鍵永遠不會被刪除,可以看做內存泄露。對於運行狀態非常依賴於內存的Redis來說,這種策略顯然會影響到Redis的性能。
(3)定期刪除:
  這種策略是對前兩種策略的整合與折中方案。使用這種策略需要控制好刪除操作每次執行的時長和執行的頻率,否則會退化為前兩種策略的其中一種。
5. Redis采用的過期鍵刪除策略
  Redis服務器實際使用的是惰性刪除和定期刪除兩種策略配合使用的方案。
(1)惰性刪除策略的實現:
  所有讀寫數據庫的Redis命令在執行之前都會先檢查輸入鍵是否已過期,過期則刪除之。
(2)定期刪除策略的實現:
  在規定時間內,分多次遍歷服務器中的各個數據庫,從數據庫的過期字典中隨機檢查一部分鍵的過期時間,並刪除其中的過期鍵。
<1>定期刪除程序每次運行時,都會從一定數量的數據庫中取出一定數量的隨機鍵進行檢查,並刪除其中的過期鍵。
<2>使用一個全局變量記錄當前刪除程序檢查的是第幾個數據庫,下一次運行都會接着上一次的進度進行處理。
<3>隨着刪除程序的不斷執行,服務器中所有的數據庫都會被檢查一遍,然后這個全局變量被重置為0,開始新一輪的檢查工作。

6. AOF、RDB和復制功能對過期鍵的處理
(1)生成RDB文件:
  在執行save或bgsave命令創建一個新的RDB文件時,程序會對數據庫中的鍵進行檢查,已過期的鍵不會被保存到新創建的RDB文件中。
(2)載入RDB文件:
<1>主服務器模式:載入RDB文件時,程序會對文件中保存的鍵進行檢查,只有未過期的鍵會被載入到數據庫中。
<2>從服務器模式:文件中保存的所有鍵都會被載入到數據庫中。不過因為主從服務器在進行數據同步的時候,從服務器的數據庫會被清空,所以過期鍵對載入RDB文件的從服務器也不會造成影響。
(3)AOF文件寫入:
  當過期鍵被惰性刪除或定期刪除之后,程序會向AOF文件追加一條del命令,來顯式地記錄該鍵已被刪除。
(4)AOF重寫:
  程序會對數據庫中的鍵進行檢查,已過期的鍵不會被保存到重寫后的AOF文件中。
(5)復制:
  當服務器運行在復制模式下時,從服務器的過期鍵刪除動作由主服務器控制:
<1>主服務器在刪除一個過期鍵之后,會顯式地向所有從服務器發送一個del命令,告知從服務器刪除這個過期鍵。
<2>從服務器在執行客戶端發送的讀命令時,即使碰到過期鍵也不會將其刪除,而是將過期鍵的值繼續返回給客戶端。
<3>從服務器只有在接到主服務器發送來的del命令之后,才會刪除過期鍵。

 

 

Redis客戶端
  Redis服務器狀態結構中的“clients”屬性是一個鏈表,保存了所有連接到當前服務器的客戶端狀態,每個客戶端狀態使用類型為“redisClient”的數據結構進行表示(每個屬性按照即將講解的順序進行排序):

//Redis客戶端的狀態結構
struct redisClient {
redisDb *db;//記錄客戶端當前正在使用的數據庫
int fd;//客戶端正在使用的套接字描述符,-1表示偽客戶端(AOF文件或者Lua腳本),大於-1表示普通客戶端
robj *name;//客戶端名字
int flags;//客戶端標志,記錄了客戶端的角色,以及客戶端目前所處的狀態
sds querybuf;//輸入緩沖區,根據輸入內容動態地縮小或擴大,但不能超過1GB,否則服務器將關閉這個客戶端
robj **argv;//命令與命令參數,數組,每個元素都是一個字符串對象,argv[0]為命令,其余元素為參數
int argc;//argv數組的長度
struct redisCommand *cmd;//當前執行的命令的實現函數,指向命令表中的命令結構
char buf[REDIS_REPLY_CHUNK_BYTES];//固定大小輸出緩沖區,數組,默認大小為16KB
int bufpos;//buf數組目前已使用的字節數量
list *reply;//可變大小輸出緩沖區,鏈表
obuf_soft_limit_reached_time:記錄了“reply”輸出緩沖區第一次到達軟性限制的時間,用於計算持續超出軟性限制的時長,以此決定是否關閉客戶端
int authenticated;//0表示未通過身份驗證,1表示已通過身份驗證
time_t ctime:創建客戶端的時間,可用於計算客戶端與服務器連接的時間長度
time_t lastinteraction:客戶端與服務器最后一次進行互動的時間,可用於客戶端的空轉時長
multiState mstate;//事務狀態,包含一個事務隊列,以及一個已入列命令計數器
};

 

“db”:
  是一個指針,指向Redis服務器狀態結構中的“db”數組其中一個元素,表示當前客戶端正在使用的數據庫。
  默認情況下,Redis客戶端的目標數據庫為0號數據庫,可以通過select命令切換,所以select命令的實現原理為:修改redisClient.db指針,讓它指向服務器中指定的數據庫。
示例圖:

“fd”:
  連接當前客戶端與Redis服務器的套接字描述符。值為-1表示偽客戶端(AOF文件或者Lua腳本),值大於-1則表示普通客戶端。
  Redis客戶端分為普通客戶端與偽客戶端兩種類型,其中通過網絡連接與Redis服務器進行連接的就是普通客戶端,反之則是偽客戶端了。偽客戶端也有兩種類型,分別是Lua腳本的偽客戶端和AOF文件的偽客戶端。Redis服務器狀態結構的“lua_client”屬性就保存了Lua腳本的偽客戶端,它會在Redis服務器初始化時就被創建,負責執行Lua腳本中包含的Redis命令,在服務器運行的整個生命周期一直存在,直至服務器關閉才會關閉。而AOF偽客戶端則是在載入AOF文件時被創建,用於執行AOF文件中的Redis命令,在AOF文件載入完成之后被關閉。
client list:列出目前所有連接到服務器的普通客戶端。
“name”:
  當前客戶端名字。
client setname:為客戶端設置一個名字。
“flags”:
  客戶端標志,記錄了客戶端的角色,以及客戶端目前所處的狀態。例如:REDIS_MASTER表示當前客戶端是一個主服務器;REDIS_BLOCKED表示當前客戶端正在被列表命令阻塞。它的值可以是單個標志,也可以是多個標志的二進制或。
“querybuf”:
  輸入緩沖區,存儲客戶端輸入的內容,可以根據輸入內容動態地縮小或擴大,但不能超過1GB,否則服務器將關閉這個客戶端。
“argv” & “argc”:
  這兩個屬性的值都是由輸入緩沖區的內容分析得來的。其中“argv”屬性是一個數組,數組的每個元素都是一個字符串對象,argv[0]為客戶端當前執行的命令,其余元素為傳給該命令的參數。而“argc”屬性則記錄了“argv”數組的長度。
“cmd”:
  當前執行的命令的實現函數,指向命令表中的命令結構。
  Redis服務器中保存着一個由字典實現的命令表,服務器會根據agrv[0]的值(不區分字母大小寫),在命令表中查找命令對應的命令實現函數,然后將“cmd”指針指向這個函數。
命令表示例圖:

redisCommand結構:保存了命令的實現函數、命令的標志、命令應該給定的參數個數,命令的總執行次數和總消耗時長等統計信息。
“buf” & “bufpos”:
  “buf”屬性是一個數組,作為固定大小的輸出緩沖區,默認大小為16KB,用於保存長度比較小的回復(服務器給客戶端的回復)。
  “bufpos”屬性則記錄了“buf”目前已經使用的字節數。
“reply”:
  鏈表,可變大小的輸出緩沖區,用於保存長度比較大的回復。
  當“buf”數組的空間已用完,或者因為回復太大而沒辦法放進“buf”數組時,服務器就會開始使用可變大小緩沖區。
  但可變大小的緩沖區也是有限制的,分為硬性限制與軟性限制兩種模式,一旦超過硬性限制服務器會立刻關閉客戶端,若是超過軟性限制,客戶端不會立刻被關閉,但若是持續一段時間一直超過軟性限制,服務器也是會關閉客戶端的。這兩種限制可以使用Redis配置的“client-output-buffer-limit”選項來進行配置:
client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
配置示例(以下分別為普通客戶端、從服務器客戶端、執行發布與訂閱功能的客戶端設置不同的軟性限制與硬性限制):
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit slave 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
“obuf_soft_limit_reached_time”:
  記錄了“reply”輸出緩沖區第一次到達軟性限制的時間,用於計算持續超出軟性限制的時長,以此決定是否關閉客戶端。
“authenticated”:
  身份驗證的標識,值為0表示未通過身份驗證,1則表示已通過身份驗證。
“ctime”:
  創建客戶端的時間,可用於計算客戶端與服務器連接的時間長度。
client list:“age”域記錄了客戶端與服務器連接的時間長度。
“lastinteraction”:
  客戶端與服務器最后一次進行互動的時間,可用於客戶端的空轉時長。
client list:“idle”域記錄了客戶端的空轉時長。

 

 

Redis事件
  Redis服務器是一個事件驅動程序,需要處理兩類事件:文件事件與時間事件。

文件事件
  Redis服務器通過套接字與客戶端或其他Redis服務器進行連接,而文件事件就是服務器對套接字操作的抽象。服務器與客戶端或其他服務器的通信會產生相應的文件事件,而服務器通過監聽並處理這些事件來完成一系列網絡通信操作。
  Redis基於Reactor模式開發了自己的網絡事件處理器——文件事件處理器,文件事件處理器使用I/O多路復用程序來同時監聽多個套接字,並根據套接字目前執行的任務來為套接字關聯不同的事件處理器。當被監聽的套接字准備好執行連接應答、讀取、寫入、關閉等操作時,與操作相對應的文件事件就會產生,這時文件事件處理器就會調用套接字之前已關聯好的事件處理器來處理這些事件。
文件事件處理器的構成:

(其中I/O多路復用程序通過隊列向文件事件分派器傳送套接字)

 

時間事件
  Redis服務器中的一些操作需要在給定時間點執行,時間事件就是服務器對這類定時操作的抽象。
1. 時間事件的分類
(1)定時事件:讓一段程序在指定的時間之后執行一次。
(2)周期性事件:讓一段程序每隔指定的時間就執行一次。
2. 時間事件的屬性
(1)id:Redis服務器為每個時間事件創建的全局唯一ID,從小到大遞增。
(2)when:毫秒精度的UNIX時間戳,記錄了時間事件的到達時間。
(3)timeProc:時間事件處理器,一個時間事件到達時就會被服務器調用的函數。
3. 時間事件的實現
服務器將所有時間事件都放在一個無序鏈表中,新加入的時間事件總是插入到鏈表的表頭中。每當時間事件執行器運行時,它就遍歷整個鏈表,查找所有已經到達的時間事件,並調用相應的事件處理器。
正常模式下Redis服務器只使用serverCron一個時間事件,而在benchmark模式下,服務器也只使用兩個時間事件。所以時間事件無序鏈表幾乎退化成一個指針,使用它來保存時間事件並不影響事件執行的性能。
4. 周期性事件serverCron
  持續運行的Redis服務器需要定期對自身的資源和狀態進行檢查和調整,從而確保服務器可以長期、穩定地運行,這些定期操作就由周期性事件serverCron來負責執行。周期性事件serverCron會每隔一段時間執行一次,直到服務器關閉為止。serverCron默認每隔100毫秒執行一次,可以通過Redis配置中的“hz”選項來設置serverCron的每秒回執行次數。
  serverCron函數主要負責執行的有以下操作:
(1)更新服務器時間緩存
  為了減少獲取系統當前時間需要執行的系統調用次數,Redis服務器使用狀態結構中的“unixtime”和“mstime”兩個屬性分別緩存秒級精度和毫秒級精度的系統當前UNIX時間戳,但是只能用於對時間精確度要求不高的功能,對時間精確度高的功能還是會執行系統調用來獲取系統當前時間。
(2)更新LRU時鍾
  使用服務器狀態的“lruclock”屬性來保存,默認每10秒更新一次,用於計算數據庫鍵的空轉時長:數據庫鍵的空轉時長 = 服務器的“lruclock”屬性值 - 數據庫鍵值對象的“lru”屬性值。
INFO SERVER:“lru_clock”域的值就是服務器狀態的“lruclock”屬性值。
(3)更新服務器每秒執行命令次數
INFO STATUS:“instantaneous_ops_per_sec”域的值就是Redis服務器在最近一秒鍾執行的命令數量。
  這個值是根據抽樣計算得到的所有結果的平均值。serverCron每100毫秒就進行一次抽樣計算,其中,Redis服務器狀態的“ops_sec_last_sample_time”屬性記錄上一次進行抽樣的時間,“ops_sec_last_sample_ops”屬性記錄上一次抽樣時服務器已執行命令的數量,“ops_sec_samples”數組則用於存放所有抽樣計算的結果,默認長度為16,“ops_sec_idx”屬性指定本次抽樣計算結果應放入“ops_sec_samples”數組的哪個索引位置,它的值在每次抽樣后自增1,等於16時重置為0。所以每次抽樣計算的過程大概如下:
  ops_sec_samples[ops_sec_idx] = (服務器當前已執行命令數量 - ops_sec_last_sample_ops屬性值) / (服務器當前時間 - ops_sec_last_sample_time屬性值) * 1000;
  ops_sec_last_sample_ops屬性值 = 服務器當前已執行命令數量;
  ops_sec_last_sample_time屬性值 = 服務器當前時間;
  ops_sec_idx ++;
  if(ops_sec_idx == 16) ops_sec_idx = 0;
  最后,服務器每秒執行命令次數 = ops_sec_samples數組元素總和 / ops_sec_samples數組長度,所以它只是一個估算值。
(4)更新服務器內存峰值記錄
  服務器狀態的“stat_peak_memory”屬性記錄了服務器已使用的內存峰值。serverCron每次執行都會比較“stat_peak_memory”屬性值與Redis服務器當前使用的內存數量,若當前使用的內存數量大於“stat_peak_memory”屬性值,則使用當前使用的內存數量更新“stat_peak_memory”屬性的值。
INFO MEMORY:“used_memory_peak”域和“used_memory_peak_human”域分別以兩種格式記錄了服務器的內存峰值。
(5)處理SIGTERM信號
  在Redis服務器啟動時,Redis會為服務器進程的SIGTERM信號關聯一個信號處理器,這個信號處理器負責載服務器收到SIGTERM信號時,將Redis服務器狀態的“shutdown_asap”屬性值置為1。
  “shutdown_asap”屬性是Redis服務器的關機標識,serverCron每次運行都會對它的值進行檢查,若其值為1則關閉Redis服務器,關閉之前會先進行RDB持久化。
(6)管理客戶端資源
  serverCron每次運行都會對一定數量的客戶端進行以下兩個檢查:
<1>如果客戶端與服務器之間的連接已經超時,即客戶端已經在很長一段時間內沒有與服務器互動,則關閉這個客戶端。
<2>若客戶端的輸入緩沖區大小超過了一定長度,則釋放當前輸入緩沖區,重新創建一個默認大小的輸入緩沖區,防止耗費過多內存。
<3>若客戶端輸出緩沖區大小超出限制,則關閉客戶端。
(7)管理數據庫資源
  serverCron每次運行都會對一部分數據庫進行檢查,刪除其中的過期鍵,並在有需要時對字典進行收縮操作。
(8)檢查持久化操作運行狀態、標識與運行條件
  Redis服務器狀態中與持久化操作的運行狀態、標識以及運行條件相關的屬性有以下六個:
<1>rdb_child_pid:記錄執行BGSAVE命令的子進程的ID,值為-1表示服務器沒有正在執行BGSAVE命令。
<2>aof_child_pid:記錄執行BGREWRITEAOF命令的子進程的ID,值為-1表示服務器沒有正在執行BGREWRITEAOF命令。
<3>aof_rewrite_scheduled:BGREWRITEAOF命令延遲執行的標識,值為1表示有BGREWRITEAOF命令被延遲了(服務器執行BGSAVE命令期間收到的BGREWRITEAOF命令請求會被延遲到BGSAVE執行完成之后執行)。
<4>saveparams:記錄了自動保存條件的數組,即執行BGSAVE的條件。可由“save”選項進行配置,例如:save 900 1,表示在900秒之內,對數據庫至少進行了1次修改,則執行BGSAVE命令。可以配置個條件。
<5>dirty:修改計數器,記錄上一次執行BGSAVE之后已經產生了多少修改。
<6>lastsave:上一次執行自動保存操作(BGSAVE)的時間。
  serverCron每次運行都會檢查“rdb_child_pid”和“aof_child_pid”兩個屬性的值,只要其中一個屬性的值不為-1,就檢查子進程是否有信號發送給服務器進程:
<1>若有信號,表示新的RDB文件已經生成完畢,或者AOF文件已經重寫完成,服務器需要進行相應命令的后續操作,比如用新的RDB文件替換現有的RDB文件,或者用重寫后的AOF文件替換現有的AOF文件。
<2>若沒信號,則表示持久化操作尚未完成,程序不做任何操作。
  如果檢查之后發現“rdb_child_pid”和“aof_child_pid”兩個屬性的值都為-1,表示服務器沒有正在進行持久化操作,這時會按以下三個步驟進行相應檢查:
<1>檢查“aof_rewrite_scheduled”屬性的值,若為1,表示有BGREWRITEAOF操作被延遲了,則開始一次新的BGREWRITEAOF操作。
<2>檢查服務器的自動保存條件是否滿足:循環取出“saveparams”數組中的所有條件配置,逐個與“dirty”和“lastsave”屬性的值進行對比,只要其中一個條件配置的時間間隔大於“dirty”屬性值,並且修改數量大於“lastsave”屬性值,則表示自動保存的條件已經滿足,若此時服務器沒有正在執行其他持久化操作,則開始一次新的BGSAVE操作。
<3>檢查服務器設置的AOF重寫條件是否滿足,如果滿足,並且服務器沒有正在執行其他持久化操作,自動開始一次新的BGREWRITEAOF操作。
整個檢查過程的流程圖:

(9)將AOF緩沖區的內容寫入AOF文件
  如果服務器開啟了AOF持久化功能,serverCron運行時會檢查AOF緩沖區“aof_buf”中有沒有內容,若有,則將AOF緩沖區中的內容寫入AOF文件中。
(10)增加cronloops計數器的值
  Redis服務器狀態的“cronloops”屬性記錄了serverCron函數執行的次數,serverCron會在每次執行之后將“cronloops”屬性的值加一。

 

事件的調度與執行
(1)獲取到達時間與當前時間最接近的時間事件。
(2)阻塞並等待文件事件產生。(避免頻繁輪詢時間事件)
(3)若有文件事件產生,則處理文件事件。
(4)若獲取的時間事件的到達時間已到,則執行時間事件,完成之后重新從步驟一開始新一輪的事件循環。
  Redis服務器對文件事件和時間事件的處理都是同步、有序、原子地執行的。因為時間事件在文件事件之后執行,並且事件之間不會出現搶占,所以時間事件的實際處理時間,通常會比時間事件設定的到達時間稍晚一些。

 

 

Redis服務器初始化步驟
(1)初始化服務器狀態結構
  創建一個struct redisServer類型的實例變量作為服務器的狀態,並為結構中的各個屬性設置默認值,例如:服務器的運行ID、默認配置文件路徑、默認端口等等,同時創建Redis命令表。
(2)載入配置選項
  載入用戶指定的配置參數和配置文件,並根據用戶設定的配置,對服務器狀態變量的相關屬性進行修改。
(3)初始化服務器數據結構
  這一步主要是為服務器狀態中的一些數據結構分配內存,例如:
<1>“clients“:鏈表,保存所有與服務器連接的客戶端的狀態結構。
<2>”db“:字典保存服務器的所有數據庫。
<3>”pubsub_channels“:字典,保存頻道訂閱信息。
<4>“pubsub_patterns”:鏈表,保存模式訂閱信息。
<5>”lua“:用於執行Lua腳本的Lua環境。
<6>”slowlog“:用於保存慢查詢日志。
  除此之外,還會進行一些非常重要的設置操作,例如:
<1>為服務器設置進程信號處理器。
<2>創建共享對象,例如經常經常用到的“OK”回復字符串對象,1到10000的字符串對象等等。
<3>為serverCron函數創建時間事件。
<4>如果AOF持久化功能已經打開,則打開現有的AOF文件,若AOF文件不存在,則創建並打開一個新的AOF文件,為AOF寫入做好准備。
<5>初始化服務器的后台I/O模塊,為將來的I/O操作做好准備。
(4)還原數據庫狀態
  若服務器啟用了AOF持久化功能,則載入AOF文件,否則載入RDB文件,根據AOF文件或RDB文件記錄的內容還原數據庫狀態,同時在日志文件中打印出載入文件並還原數據庫狀態所耗費的時長。
(5)執行事件循環
  一切准備就緒,開始執行服務器的事件循環,開始接受客戶端的連接請求,處理客戶端發送的命令請求。

 


Redis命令請求的執行過程
(1)發送命令請求
  當用戶在客戶端中鍵入一個命令請求時,客戶端會將這個命令請求轉換成協議格式,然后通過連接到服務器的套接字,將協議格式的命令請求發送給服務器。
(2)讀取命令請求
  當客戶端與服務器之間的連接套接字因為客戶端的寫入而變得可讀時,服務器將調用命令請求處理器來執行以下操作:
<1>讀取套接字中協議格式的命令請求,並將其保存到客戶端狀態的輸入緩沖區“querybuf”中。
<2>對輸入緩沖區中的命令請求進行分析,提取出命令請求中包含的命令參數,計算命令參數的個數,然后分別將它們保存到客戶端狀態的“argv”屬性和“argc”屬性中。
<3>調用命令執行器,執行客戶端指定的命令。
(3)命令執行器——查找命令
  根據客戶端狀態的argv[0]參數,在命令表中查找參數所指定的命令(查找結果不受命令名字大小寫影響),並將其保存到客戶端狀態的“cmd”屬性中。(命令表是一個字典,字典的鍵是命令的名字,字典的值是一個“redisCommand”結構,記錄着Redis命令的實現函數與一些統計信息)
設置客戶端狀態的“cmd”屬性的示例圖:

(4)命令執行器——執行預備操作
<1>檢查客戶端狀態的“cmd”指針是否指向NULL,以此判斷用戶輸入的命令是否存在。
<2>根據客戶端狀態的“cmd”屬性指向的“redisCommand”結構中的“arity”屬性值和客戶端狀態的“argc”屬性值,判斷用戶輸入的命令參數個數是否正確。
<3>通過客戶端狀態的“authenticated”屬性值判斷客戶端是否已經通過了身份驗證,未通過只能執行AUTH命令。
<4>如果服務器打開了“maxmemory”功能,在執行命令之前,需要先檢查服務器的內存占用情況,並在有需要時進行內存回收,若內存回收失敗則不再執行后續步驟,向客戶端返回一個錯誤。
<5>如果服務器上一次執行BGSAVE命令時出錯,並且服務器打開了"stop-writes-on-bgsave-error"功能, 而且服務器即將要執行的命令是一個寫命令,那么服務器將拒絕執行這個命令,並向客戶端返回一個錯誤。
<6>如果客戶端當前正在用SUBSCRIBE命令訂閱頻道,或者正在用PSUBSCRIBE命令訂閱模式, 那么服務器只會執行客戶端發來的SUBSCRIBE 、PSUBSCRIBE 、UNSUBSCRIBE 、PUNSUBSCRIBE四個命令,其他別的命令都會被服務器拒絕。
<7>如果服務器正在進行數據載入,那么客戶端發送的命令必須帶有“l”標識(比如INFO 、SHUTDOWN 、PUBLISH,等等)才會被服務器執行,其他別的命令都會被服務器拒絕。
<8>如果服務器因為執行Lua腳本而超時並進入阻塞狀態,那么服務器只會執行客戶端發來的SHUTDOWN nosave命令和SCRIPT KILL命令,其他別的命令都會被服務器拒絕。
<9>如果客戶端正在執行事務,那么服務器只會執行客戶端發來的EXEC 、DISCARD 、MULTI 、WATCH四個命令,其他命令都會被放進事務隊列中。
<10>如果服務器打開了監視器功能,那么服務器會將要執行的命令和參數等信息發送給監視器。
(5)命令執行器——調用命令的實現函數
client->cmd->proc(client);//client是指向客戶端狀態的指針
  調用實現函數執行指定操作,產生的相應的命令回復,將其保存到客戶端狀態的輸出緩沖區中,並為客戶端的套接字關聯命令回復處理器,這個處理器負責將命令回復返回給客戶端。
(6)命令執行器——執行后續工作
<1>如果服務器開啟了慢查詢日志功能,那么慢查詢日志模塊會檢查是否需要為剛剛執行完的命令請求添加一條新的慢查詢日志。
<2>根據剛剛執行命令所耗費的時長,更新被執行命令的“redisCommand”結構的“milliseconds”屬性, 並將命令的“redisCommand”結構的“calls”計數器的值增一。
<3>如果服務器開啟了AOF持久化功能, 那么AOF持久化模塊會將剛剛執行的命令請求寫入到AOF緩沖區里面。
<4>如果有其他從服務器正在復制當前這個服務器,那么服務器會將剛剛執行的命令傳播給所有從服務器。
(7)將命令回復發送給客戶端
  當客戶端套接字變為可寫時,服務器就會執行命令回復處理器,將保存在客戶端輸出緩沖區的命令回復發送給客戶端。發送完畢之后,清空客戶端輸出緩沖區。
(8)客戶端接受並打印命令回復
  客戶端接收到協議格式的命令回復之后,將其轉換成人類可讀的格式,並打印在客戶端屏幕上。

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM