7. pexpire key milliseconds O(1)
10.SORT key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC | DESC] [ALPHA] [STORE destination]
2. 用做Hash 類型值的其中一種底層實現
事務提供了一種「將多個命令打包,而後一次性、按順序地執行」的機制,而且事務在執行的期
間不會主動中斷——服務器在執行完事務中的全部命令以後,纔會繼續處理其餘客戶端的其餘
命令
MULTI 命令的執行標記着事務的開始:
redis> MULTI
OK
這個命令惟一作的就是,將客戶端的REDIS_MULTI 選項打開,讓客戶端從非事務狀態切換到事
務狀態
當客戶端進入事務狀態以後,服務器在收到來自客戶端的命令時,不會當即執行命令,
而是將這些命令所有放進一個事務隊列裏,事務隊列是一個數組,每一個數組項是都包含三個屬性:
1. 要執行的命令(cmd)。
2. 命令的參數(argv)。
3. 參數的個數(argc)
舉個例子,若是客戶端執行如下命令:
redis> MULTI
OK
redis> SET book-name "Mastering C++ in 21 days"
QUEUED
redis> GET book-name
QUEUED
redis> SADD tag "C++" "Programming" "Mastering Series"
QUEUED
redis> SMEMBERS tag
QUEUED
那麼程序將爲客戶端建立如下事務隊列:
其實並非全部的命令都會被放進事務隊列,其中的例外就是EXEC 、DISCARD 、MULTI
和WATCH 這四個命令——當這四個命令從客戶端發送到服務器時,它們會像客戶端處於非
事務狀態同樣,直接被服務器執行
DISCARD 命令用於取消一個事務,它清空客戶端的整個事務隊列,而後將客戶端從事務狀態
調整回非事務狀態,最後返回字符串OK 給客戶端,說明事務已被取消。
Redis 的事務是不可嵌套的,當客戶端已經處於事務狀態,而客戶端又再向服務器發送MULTI
時,服務器只是簡單地向客戶端發送一個錯誤,而後繼續等待其餘命令的入隊。MULTI 命令
的發送不會形成整個事務失敗,也不會修改事務隊列中已有的數據。
WATCH 只能在客戶端進入事務狀態以前執行,在事務狀態下發送WATCH 命令會引起一個
錯誤,但它不會形成整個事務失敗,也不會修改事務隊列中已有的數據(和前面處理MULTI
的狀況同樣)。
WATCH 命令的實現
在每一個表明數據庫的redis.h/redisDb 結構類型中,都保存了一個watched_keys 字典,字典
的鍵是這個數據庫被監視的鍵,而字典的值則是一個鏈表,鏈表中保存了全部監視這個鍵的客
戶端。
在任何對數據庫鍵空間(key space)進行修改的命令成功執行以後(好比FLUSHDB 、SET
、DEL 、LPUSH 、SADD 、ZREM ,諸如此類),multi.c/touchWatchKey 函數都會被調用
——它檢查數據庫的watched_keys 字典,看是否有客戶端在監視已經被命令修改的鍵,若是
有的話,程序將全部監視這個/這些被修改鍵的客戶端的REDIS_DIRTY_CAS 選項打開
當客戶端發送EXEC 命令、觸發事務執行時,服務器會對客戶端的狀態進行檢查:
• 若是客戶端的REDIS_DIRTY_CAS 選項已經被打開,那麼說明被客戶端監視的鍵至少有一
個已經被修改了,事務的安全性已經被破壞。服務器會放棄執行這個事務,直接向客戶端
返回空回覆,表示事務執行失敗。
• 若是REDIS_DIRTY_CAS 選項沒有被打開,那麼說明全部監視鍵都安全,服務器正式執行
事務。
最後,當一個客戶端結束它的事務時,不管事務是成功執行,仍是失敗,watched_keys 字典
中和這個客戶端相關的資料都會被清除
在傳統的關係式數據庫中,經常用ACID 性質來檢驗事務功能的安全性。
Redis 事務保證了其中的一致性(C)和隔離性(I),但並不保證原子性(A)和持久性(D)
單個Redis 命令的執行是原子性的,但Redis 沒有在事務上增長任何維持原子性的機制,因此
Redis 事務的執行並非原子性的。
若是一個事務隊列中的全部命令都被成功地執行,那麼稱這個事務執行成功。
另外一方面,若是Redis 服務器進程在執行事務的過程當中被中止——好比接到KILL 信號、宿主
機器停機,等等,那麼事務執行失敗。
當事務失敗時,
Redis 也不會進行任何的重試或者回滾動做
Redis 的一致性問題能夠分爲三部分來討論:入隊錯誤、執行錯誤、Redis 進程被終結。
入隊錯誤
在命令入隊的過程當中, 若是客戶端向服務器發送了錯誤的命令, 好比命令的參數數量
不對,等等,那麼服務器將向客戶端返回一個出錯信息,而且將客戶端的事務狀態設爲
REDIS_DIRTY_EXEC 。
當客戶端執行EXEC 命令時,Redis 會拒絕執行狀態爲REDIS_DIRTY_EXEC 的事務,並返回失
敗信息。
所以,帶有不正確入隊命令的事務不會被執行,也不會影響數據庫的一致性。
執行錯誤
若是命令在事務執行的過程當中發生錯誤,好比說,對一個不一樣類型的key 執行了錯誤的操做,
那麼Redis 只會將錯誤包含在事務的結果中,這不會引發事務中斷或整個失敗,不會影響已執
行事務命令的結果,也不會影響後面要執行的事務命令,因此它對事務的一致性也沒有影響。
Redis 進程被終結
若是Redis 服務器進程在執行事務的過程當中被其餘進程終結,或者被管理員強制殺死,那麼根
據Redis 所使用的持久化模式,可能有如下狀況出現:
• 內存模式:若是Redis 沒有采起任何持久化機制,那麼重啓以後的數據庫老是空白的,所
以數據老是一致的。
• RDB 模式:在執行事務時,Redis 不會中斷事務去執行保存RDB 的工做,只有在事務執
行以後,保存RDB 的工做纔有可能開始。因此當RDB 模式下的Redis 服務器進程在事
務中途被殺死時,事務內執行的命令,無論成功了多少,都不會被保存到RDB 文件裏。
恢復數據庫須要使用現有的RDB 文件,而這個RDB 文件的數據保存的是最近一次的數
據庫快照(snapshot),因此它的數據可能不是最新的,但只要RDB 文件自己沒有由於
其餘問題而出錯,那麼還原後的數據庫就是一致的。
• AOF 模式:由於保存AOF 文件的工做在後臺線程進行,因此即便是在事務執行的中途,
保存AOF 文件的工做也能夠繼續進行,所以,根據事務語句是否被寫入並保存到AOF
文件,有如下兩種狀況發生:
1)若是事務語句未寫入到AOF 文件,或AOF 未被SYNC 調用保存到磁盤,那麼當進
程被殺死以後,Redis 能夠根據最近一次成功保存到磁盤的AOF 文件來還原數據庫,只
要AOF 文件自己沒有由於其餘問題而出錯,那麼還原後的數據庫老是一致的,但其中的
數據不必定是最新的。
2)若是事務的部分語句被寫入到AOF 文件,而且AOF 文件被成功保存,那麼不完整的
事務執行信息就會遺留在AOF 文件裏,當重啓Redis 時,程序會檢測到AOF 文件並不
完整,Redis 會退出,並報告錯誤。須要使用redis-check-aof 工具將部分紅功的事務命令
移除以後,才能再次啓動服務器。還原以後的數據老是一致的,並且數據也是最新的(直
到事務執行以前爲止)。
隔離性(Isolation)
Redis 是單進程程序,而且它保證在執行事務時,不會對事務進行中斷,事務能夠運行直到執
行完全部事務隊列中的命令爲止。所以,Redis 的事務是老是帶有隔離性的。
持久性(Durability)
由於事務不過是用隊列包裹起了一組Redis 命令,並無提供任何額外的持久性功能,因此事
務的持久性由Redis 所使用的持久化模式決定:
• 在單純的內存模式下,事務確定是不持久的。
• 在RDB 模式下,服務器可能在事務執行以後、RDB 文件更新以前的這段時間失敗,所
以RDB 模式下的Redis 事務也是不持久的。
• 在AOF 的「老是SYNC 」模式下,事務的每條命令在執行成功以後,都會當即調用fsync
或fdatasync 將事務數據寫入到AOF 文件。可是,這種保存是由後臺線程進行的,主
線程不會阻塞直到保存成功,因此從命令執行成功到數據保存到硬盤之間,仍是有一段
很是小的間隔,因此這種模式下的事務也是不持久的。
其餘AOF 模式也和「老是SYNC 」模式相似,因此它們都是不持久的
• 事務提供了一種將多個命令打包,而後一次性、有序地執行的機制。
• 事務在執行過程當中不會被中斷,全部事務命令執行完以後,事務才能結束。
• 多個命令會被入隊到事務隊列中,而後按先進先出(FIFO)的順序執行。
• 帶WATCH 命令的事務會將客戶端和被監視的鍵在數據庫的watched_keys 字典中進行關
聯,當鍵被修改時,程序會將全部監視被修改鍵的客戶端的REDIS_DIRTY_CAS 選項打開。
• 只有在客戶端的REDIS_DIRTY_CAS 選項未被打開時,才能執行事務,不然事務直接返回
失敗。
• Redis 的事務保證了ACID 中的一致性(C)和隔離性(I),但並不保證原子性(A)和
持久性(D)
redis頻道與訂閱
每一個Redis 服務器進程都維持着一個表示服務器狀態的redis.h/redisServer 結構,結構的
pubsub_channels 屬性是一個字典,這個字典就用於保存訂閱頻道的信息:
struct redisServer {
// ...
dict *pubsub_channels;
// ...
};複製代碼
其中,字典的鍵爲正在被訂閱的頻道,而字典的值則是一個鏈表,鏈表中保存了全部訂閱這個
頻道的客戶端。
當客戶端調用SUBSCRIBE 命令時,程序就將客戶端和要訂閱的頻道在pubsub_channels 字
典中關聯起來。
瞭解了pubsub_channels 字典的結構以後,解釋PUBLISH 命令的實現就很是簡單了:當調
用PUBLISH channel message 命令,程序首先根據channel 定位到字典的鍵,而後將信息發
送給字典值鏈表中的全部客戶端。
使用UNSUBSCRIBE 命令能夠退訂指定的頻道,這個命令執行的是訂閱的反操做:它從
pubsub_channels 字典的給定頻道(鍵)中,刪除關於當前客戶端的信息,這樣被退訂頻道的
信息就不會再發送給這個客戶端。
struct redisServer {
// ...
list *pubsub_patterns;
// ...
};複製代碼
redisServer.pubsub_patterns 屬性是一個鏈表,鏈表中保存着全部和模式相關的信息:
鏈表中的每一個節點都包含一個redis.h/pubsubPattern 結構
typedef struct pubsubPattern {
redisClient *client;
robj *pattern;
} pubsubPattern;複製代碼
client 屬性保存着訂閱模式的客戶端,而pattern 屬性則保存着被訂閱的模式。
每當調用PSUBSCRIBE 命令訂閱一個模式時,程序就建立一個包含客戶端信息和被訂閱模式的
pubsubPattern 結構,並將該結構添加到redisServer.pubsub_patterns 鏈表中。
做爲例子,下圖展現了一個包含兩個模式的pubsub_patterns 鏈表,其中client123 和
client256 都正在訂閱tweet.shop.* 模式
經過遍歷整個pubsub_patterns 鏈表,程序能夠檢查全部正在被訂閱的模式,以及訂閱這些模
式的客戶端。
退訂模式
使用PUNSUBSCRIBE 命令能夠退訂指定的模式,這個命令執行的是訂閱模式的反操做:程序
會刪除redisServer.pubsub_patterns 鏈表中,全部和被退訂模式相關聯的pubsubPattern
結構,這樣客戶端就不會再收到和模式相匹配的頻道發來的信息。
• 訂閱信息由服務器進程維持的redisServer.pubsub_channels 字典保存,字典的鍵爲被
訂閱的頻道,字典的值爲訂閱頻道的全部客戶端。
• 當有新消息發送到頻道時,程序遍歷頻道(鍵)所對應的(值)全部客戶端,而後將消息
發送到全部訂閱頻道的客戶端上。
• 訂閱模式的信息由服務器進程維持的redisServer.pubsub_patterns 鏈表保存,鏈表的
每一個節點都保存着一個pubsubPattern 結構,結構中保存着被訂閱的模式,以及訂閱該
模式的客戶端。程序經過遍歷鏈表來查找某個頻道是否和某個模式匹配。
• 當有新消息發送到頻道時,除了訂閱頻道的客戶端會收到消息以外,全部訂閱了匹配頻
道的模式的客戶端,也一樣會收到消息。
• 退訂頻道和退訂模式分別是訂閱頻道和訂閱模式的反操做。
慢日誌
每條慢查詢日誌都以一個slowlog.h/slowlogEntry 結構定義:
typedef struct slowlogEntry {
// 命令參數
robj **argv;
// 命令參數數量
int argc;
// 惟一標識符
long long id;
// 執行命令消耗的時間,以納秒(1 / 1,000,000,000 秒)爲單位
long long duration;
// 命令執行時的時間
time_t time;
} slowlogEntry;
記錄服務器狀態的redis.h/redisServer 結構裏保存了幾個和慢查詢有關的屬性:
struct redisServer {
// ... other fields
// 保存慢查詢日誌的鏈表
list *slowlog;
// 慢查詢日誌的當前id 值
long long slowlog_entry_id;
// 慢查詢時間限制
long long slowlog_log_slower_than;
// 慢查詢日誌的最大條目數量
unsigned long slowlog_max_len;
// ... other fields
};
slowlog 屬性是一個鏈表,鏈表裏的每一個節點保存了一個慢查詢日誌結構,全部日誌按添加時
間重新到舊排序,新的日誌在鏈表的左端,舊的日誌在鏈表的右端。
slowlog_entry_id 在建立每條新的慢查詢日誌時增一,用於產生慢查詢日誌的ID (這個ID
在執行SLOWLOG RESET 以後會被重置)。
slowlog_log_slower_than 是用戶指定的命令執行時間上限,執行時間大於等於這個值的命令
會被慢查詢日誌記錄。
slowlog_max_len 慢查詢日誌的最大數量,當日志數量等於這個值時,添加一條新日誌會形成
最舊的一條日誌被刪除。
在每次執行命令以前,Redis 都會用一個參數記錄命令執行前的時間,在命令執行完以後,再
計算一次當前時間,而後將兩個時間值相減,得出執行命令所耗費的時間值duration ,並將
duration 傳給slowlogPushEntryIfNeed 函數。
針對慢查詢日誌有三種操做,分別是查看、清空和獲取日誌數量:
• 查看日誌:在日誌鏈表中遍歷指定數量的日誌節點,複雜度爲O(N) 。
• 清空日誌:釋放日誌鏈表中的全部日誌節點,複雜度爲O(N) 。
• 獲取日誌數量:獲取日誌的數量等同於獲取server.slowlog 鏈表的數量,複雜度爲
O(1) 。
• Redis 用一個鏈表以FIFO 的順序保存着全部慢查詢日誌。
• 每條慢查詢日誌以一個慢查詢節點表示,節點中記錄着執行超時的命令、命令的參數、命
令執行時的時間,以及執行命令所消耗的時間等信息。
【第五章 內部運做機制】
Redis 中的每一個數據庫,都由一個redis.h/redisDb 結構表示:
typedef struct redisDb {
// 保存着數據庫以整數表示的號碼
int id;
// 保存着數據庫中的全部鍵值對數據
// 這個屬性也被稱爲鍵空間(key space)
dict *dict;
// 保存着鍵的過時信息
dict *expires;
// 實現列表阻塞原語,如BLPOP
// 在列表類型一章有詳細的討論
dict *blocking_keys;
dict *ready_keys;
// 用於實現WATCH 命令
// 在事務章節有詳細的討論
dict *watched_keys;
} redisDb;
redisDb 結構的id 域保存着數據庫的號碼。
這個號碼很容易讓人將它和切換數據庫的SELECT 命令聯繫在一塊兒,可是,實際上,id 屬性
並非用來實現SELECT 命令,而是給Redis 內部程序使用的。
當Redis 服務器初始化時, 它會建立出redis.h/REDIS_DEFAULT_DBNUM 個數據庫, 並
將全部數據庫保存到redis.h/redisServer.db 數組中, 每一個數據庫的id 爲從0 到
REDIS_DEFAULT_DBNUM - 1 的值。
當執行SELECT number 命令時,程序直接使用redisServer.db[number] 來切換數據庫。
可是,一些內部程序,好比AOF 程序、複製程序和RDB 程序,須要知道當前數據庫的號碼,
若是沒有id 域的話,程序就只能在當前使用的數據庫的指針,和redisServer.db 數組中所
有數據庫的指針進行對比,以此來弄清楚本身正在使用的是那個數據庫。
在數據庫中,全部鍵的過時時間都被保存在redisDb 結構的expires 字典裏:
typedef struct redisDb {
// ...
dict *expires;
// ...
} redisDb;複製代碼
expires 字典的鍵是一個指向dict 字典(鍵空間)裏某個鍵的指針,而字典的值則是鍵所指
向的數據庫鍵的到期時間,這個值以long long 類型表示。
下圖展現了一個含有三個鍵的數據庫,其中number 和book 兩個鍵帶有過時時間:
Redis 有四個命令能夠設置鍵的生存時間(能夠存活多久)和過時時間(何時到期):
• EXPIRE 以秒爲單位設置鍵的生存時間;
• PEXPIRE 以毫秒爲單位設置鍵的生存時間;
• EXPIREAT 以秒爲單位,設置鍵的過時UNIX 時間戳;
• PEXPIREAT 以毫秒爲單位,設置鍵的過時UNIX 時間戳。
雖然有那麼多種不一樣單位和不一樣形式的設置方式,可是expires 字典的值只保存「以毫秒爲單
位的過時UNIX 時間戳」 ,這就是說,經過進行轉換,全部命令的效果最後都和PEXPIREAT
命令的效果同樣。
舉個例子,從EXPIRE 命令到PEXPIREAT 命令的轉換能夠用僞代碼表示以下:
def EXPIRE(key, sec):
# 將TTL 從秒轉換爲毫秒
ms = sec_to_ms(sec)
# 獲取以毫秒計算的當前UNIX 時間戳
ts_in_ms = get_current_unix_timestamp_in_ms()
# 毫秒TTL 加上毫秒時間戳,就是key 到期的時間戳
PEXPIREAT(ms + ts_in_ms, key)
其餘函數的轉換方式也是相似的。
做爲例子,下圖展現了一個expires 字典示例,字典中number 鍵的過時時間是2013 年2 月
10 日(農曆新年),而book 鍵的過時時間則是2013 年2 月14 日(情人節):
經過expires 字典,能夠用如下步驟檢查某個鍵是否過時:
1. 檢查鍵是否存在於expires 字典:若是存在,那麼取出鍵的過時時間;
2. 檢查當前UNIX 時間戳是否大於鍵的過時時間:若是是的話,那麼鍵已通過期;不然,
鍵未過時。
能夠用僞代碼來描述這一過程:
def is_expired(key):
key_expire_time = expires.get(key)
if expire_time is not None and current_timestamp() > key_expire_time:
return True
return False複製代碼
咱們知道了過時時間保存在expires 字典裏,又知道了該如何斷定一個鍵是否過時,如今剩下
的問題是,
過時鍵刪除
若是一個鍵是過時的,那它何時會被刪除?
這個問題有三種可能的答案:
1. 定時刪除:在設置鍵的過時時間時,建立一個定時事件,當過時時間到達時,由事件處理
器自動執行鍵的刪除操做。
2. 惰性刪除:聽任鍵過時無論,可是在每次從dict 字典中取出鍵值時,要檢查鍵是否過
期,若是過時的話,就刪除它,並返回空;若是沒過時,就返回鍵值。
3. 按期刪除:每隔一段時間,對expires 字典進行檢查,刪除裏面的過時鍵。
定時刪除
定時刪除策略對內存是最友好的:由於它保證過時鍵會在第一時間被刪除,過時鍵所消耗的內
存會當即被釋放。
這種策略的缺點是,它對CPU 時間是最不友好的:由於刪除操做可能會佔用大量的CPU 時間
——在內存不緊張、可是CPU 時間很是緊張的時候(好比說,進行交集計算或排序的時候),
將CPU 時間花在刪除那些和當前任務無關的過時鍵上,這種作法毫無疑問會是低效的。
除此以外,目前Redis 事件處理器對時間事件的實現方式——無序鏈表,查找一個時間複雜度
爲O(N) ——並不適合用來處理大量時間事件。
惰性刪除
惰性刪除對CPU 時間來講是最友好的:它只會在取出鍵時進行檢查,這能夠保證刪除操做只
會在非作不可的狀況下進行——而且刪除的目標僅限於當前處理的鍵,這個策略不會在刪除其
他無關的過時鍵上花費任何CPU 時間。
惰性刪除的缺點是,它對內存是最不友好的:若是一個鍵已通過期,而這個鍵又仍然保留在數
據庫中,那麼dict 字典和expires 字典都須要繼續保存這個鍵的信息,只要這個過時鍵不被
刪除,它佔用的內存就不會被釋放。
在使用惰性刪除策略時,若是數據庫中有很是多的過時鍵,但這些過時鍵又正好沒有被訪問的
話,那麼它們就永遠也不會被刪除(除非用戶手動執行),這對於性能很是依賴於內存大小的
Redis 來講,確定不是一個好消息。
舉個例子,對於一些按時間點來更新的數據,好比日誌(log),在某個時間點以後,對它們的訪
問就會大大減小,若是大量的這些過時數據積壓在數據庫裏面,用戶覺得它們已通過期了(已
經被刪除了),但實際上這些鍵卻沒有真正的被刪除(內存也沒有被釋放),那結果確定是很是
糟糕。
按期刪除
從上面對定時刪除和惰性刪除的討論來看,這兩種刪除方式在單一使用時都有明顯的缺陷:定
時刪除佔用太多CPU 時間,惰性刪除浪費太多內存。
按期刪除是這兩種策略的一種折中:
• 它每隔一段時間執行一次刪除操做,並經過限制刪除操做執行的時長和頻率,籍此來減
少刪除操做對CPU 時間的影響。
• 另外一方面,經過按期刪除過時鍵,它有效地減小了因惰性刪除而帶來的內存浪費。
Redis 使用的過時鍵刪除策略是惰性刪除加上按期刪除,這兩個策略相互配合,能夠很好地在
合理利用CPU 時間和節約內存空間之間取得平衡。
實現過時鍵惰性刪除策略的核心是db.c/expireIfNeeded 函數——全部命令在讀取或寫入數
據庫以前,程序都會調用expireIfNeeded 對輸入鍵進行檢查,並將過時鍵刪除
對過時鍵的按期刪除由redis.c/activeExpireCycle 函執行:每當Redis 的例行處理程序
serverCron 執行時,activeExpireCycle 都會被調用——這個函數在規定的時間限制內,盡
可能地遍歷各個數據庫的expires 字典,隨機地檢查一部分鍵的過時時間,並刪除其中的過時
鍵。
過時鍵對AOF 、RDB 和複製的影響
更新後的RDB 文件
在建立新的RDB 文件時,程序會對鍵進行檢查,過時的鍵不會被寫入到更新後的RDB 文件
中。
所以,過時鍵對更新後的RDB 文件沒有影響
AOF 文件
在鍵已通過期,可是尚未被惰性刪除或者按期刪除以前,這個鍵不會產生任何影響,AOF 文
件也不會由於這個鍵而被修改。
當過時鍵被惰性刪除、或者按期刪除以後,程序會向AOF 文件追加一條DEL 命令,來顯式地
記錄該鍵已被刪除。
舉個例子,若是客戶端使用GET message 試圖訪問message 鍵的值,但message 已通過期了,
那麼服務器執行如下三個動做:
1. 從數據庫中刪除message ;
2. 追加一條DEL message 命令到AOF 文件;
3. 向客戶端返回NIL
AOF 重寫
和RDB 文件相似,當進行AOF 重寫時,程序會對鍵進行檢查,過時的鍵不會被保存到重寫
後的AOF 文件。
所以,過時鍵對重寫後的AOF 文件沒有影響。
複製
當服務器帶有附屬節點時,過時鍵的刪除由主節點統一控制:
• 若是服務器是主節點,那麼它在刪除一個過時鍵以後,會顯式地向全部附屬節點發送一
個DEL 命令。
• 若是服務器是附屬節點,那麼當它碰到一個過時鍵的時候,它會向程序返回鍵已過時的
回覆,但並不真正的刪除過時鍵。由於程序只根據鍵是否已通過期、而不是鍵是否已經被
刪除來決定執行流程,因此這種處理並不影響命令的正確執行結果。當接到從主節點發來
的DEL 命令以後,附屬節點纔會真正的將過時鍵刪除掉。
附屬節點不自主對鍵進行刪除是爲了和主節點的數據保持絕對一致,由於這個緣由,當一個過
期鍵還存在於主節點時,這個鍵在全部附屬節點的副本也不會被刪除。
這種處理機制對那些使用大量附屬節點,而且帶有大量過時鍵的應用來講,可能會形成一部分
內存不能當即被釋放,可是,由於過時鍵一般很快會被主節點發現並刪除,因此這實際上也算
不上什麼大問題。
數據庫空間的收縮和擴展
由於數據庫空間是由字典來實現的,因此數據庫空間的擴展/收縮規則和字典的擴展/收縮規則
徹底同樣,具體的信息能夠參考《字典》章節。
由於對字典進行收縮的時機是由使用字典的程序決定的, 因此Redis 使用
redis.c/tryResizeHashTables 函數來檢查數據庫所使用的字典是否須要進行收縮:每次
redis.c/serverCron 函數運行的時候,這個函數都會被調用。
tryResizeHashTables 函數的完整定義以下
void tryResizeHashTables(void) {
int j;
for (j = 0; j < server.dbnum; j++) {
// 縮小鍵空間字典
if (htNeedsResize(server.db[j].dict))
dictResize(server.db[j].dict);
// 縮小過時時間字典
if (htNeedsResize(server.db[j].expires))
dictResize(server.db[j].expires);
}
}
• 數據庫主要由dict 和expires 兩個字典構成,其中dict 保存鍵值對,而expires 則
保存鍵的過時時間。
• 數據庫的鍵老是一個字符串對象,而值能夠是任意一種Redis 數據類型,包括字符串、哈
希、集合、列表和有序集。
• expires 的某個鍵和dict 的某個鍵共同指向同一個字符串對象,而expires 鍵的值則
是該鍵以毫秒計算的UNIX 過時時間戳。
• Redis 使用惰性刪除和按期刪除兩種策略來刪除過時的鍵。
• 更新後的RDB 文件和重寫後的AOF 文件都不會保留已通過期的鍵。
• 當一個過時鍵被刪除以後,程序會追加一條新的DEL 命令到現有AOF 文件末尾。
• 當主節點刪除一個過時鍵以後,它會顯式地發送一條DEL 命令到全部附屬節點。
• 附屬節點即便發現過時鍵,也不會自做主張地刪除它,而是等待主節點發來DEL 命令,
這樣能夠保證主節點和附屬節點的數據老是一致的。
• 數據庫的dict 字典和expires 字典的擴展策略和普通字典同樣。它們的收縮策略是:當
節點的填充百分比不足10% 時,將可用節點數量減小至大於等於當前已用節點數量。
持久化
在Redis 運行時,RDB 程序將當前內存中的數據庫快照保存到磁盤文件中,在Redis 重啓動
時,RDB 程序能夠經過載入RDB 文件來還原數據庫的狀態
RDB 功能最核心的是rdbSave 和rdbLoad 兩個函數,前者用於生成RDB 文件到磁盤,然後
者則用於將RDB 文件中的數據從新載入到內存中:
rdbSave 函數負責將內存中的數據庫數據以RDB 格式保存到磁盤中,若是RDB 文件已存在,
那麼新的RDB 文件將替換已有的RDB 文件。
在保存RDB 文件期間,主進程會被阻塞,直到保存完成爲止。
SAVE 和BGSAVE 兩個命令都會調用rdbSave 函數,但它們調用的方式各有不一樣:
• SAVE 直接調用rdbSave ,阻塞Redis 主進程,直到保存完成爲止。在主進程阻塞期間,
服務器不能處理客戶端的任何請求。
• BGSAVE 則fork 出一個子進程,子進程負責調用rdbSave ,並在保存完成以後向主
進程發送信號,通知保存已完成。由於rdbSave 在子進程被調用,因此Redis 服務器在
BGSAVE 執行期間仍然能夠繼續處理客戶端的請求
SAVE 、BGSAVE 、AOF 寫入和BGREWRITEAOF
除了瞭解RDB 文件的保存方式以外,咱們可能還想知道,兩個RDB 保存命令可否同時使用?
它們和AOF 保存工做是否衝突?
SAVE
前面提到過,當SAVE 執行時,Redis 服務器是阻塞的,因此當SAVE 正在執行時,新的
SAVE 、BGSAVE 或BGREWRITEAOF 調用都不會產生任何做用。
只有在上一個SAVE 執行完畢、Redis 從新開始接受請求以後,新的SAVE 、BGSAVE 或
BGREWRITEAOF 命令纔會被處理。
另外,由於AOF 寫入由後臺線程完成,而BGREWRITEAOF 則由子進程完成,因此在SAVE
執行的過程當中,AOF 寫入和BGREWRITEAOF 能夠同時進行。
BGSAVE
在執行SAVE 命令以前,服務器會檢查BGSAVE 是否正在執行當中,若是是的話,服務器就
不調用rdbSave ,而是向客戶端返回一個出錯信息,告知在BGSAVE 執行期間,不能執行
SAVE 。
這樣作能夠避免SAVE 和BGSAVE 調用的兩個rdbSave 交叉執行,形成競爭條件。
另外一方面,當BGSAVE 正在執行時,調用新BGSAVE 命令的客戶端會收到一個出錯信息,告
知BGSAVE 已經在執行當中。
BGREWRITEAOF 和BGSAVE 不能同時執行:
• 若是BGSAVE 正在執行,那麼BGREWRITEAOF 的重寫請求會被延遲到BGSAVE 執
行完畢以後進行,執行BGREWRITEAOF 命令的客戶端會收到請求被延遲的回覆。
• 若是BGREWRITEAOF 正在執行,那麼調用BGSAVE 的客戶端將收到出錯信息,表示
這兩個命令不能同時執行。
BGREWRITEAOF 和BGSAVE 兩個命令在操做方面並無什麼衝突的地方,不能同時執行
它們只是一個性能方面的考慮:併發出兩個子進程,而且兩個子進程都同時進行大量的磁盤寫
入操做,這怎麼想都不會是一個好主意。
載入
當Redis 服務器啓動時,rdbLoad 函數就會被執行,它讀取RDB 文件,並將文件中的數據庫
數據載入到內存中。
在載入期間,服務器每載入1000 個鍵就處理一次全部已到達的請求,不過只有PUBLISH 、
SUBSCRIBE 、PSUBSCRIBE 、UNSUBSCRIBE 、PUNSUBSCRIBE 五個命令的請求會被正確地處理,
其餘命令一概返回錯誤。等到載入完成以後,服務器纔會開始正常處理全部命令。
Note: 發佈與訂閱功能和其餘數據庫功能是徹底隔離的,前者不寫入也不讀取數據庫,因此
在服務器載入期間,訂閱與發佈功能仍然能夠正常使用,而沒必要擔憂對載入數據的完整性產生
影響。
另外,由於AOF 文件的保存頻率一般要高於RDB 文件保存的頻率,因此通常來講,AOF 文
件中的數據會比RDB 文件中的數據要新。
所以,若是服務器在啓動時,打開了AOF 功能,那麼程序優先使用AOF 文件來還原數據。只
有在AOF 功能未打開的狀況下,Redis 纔會使用RDB 文件來還原數據
前面介紹了保存和讀取RDB 文件的兩個函數,如今,是時候介紹RDB 文件自己了。
一個RDB 文件能夠分爲如下幾個部分:
REDIS
文件的最開頭保存着REDIS 五個字符,標識着一個RDB 文件的開始。
在讀入文件的時候,程序能夠經過檢查一個文件的前五個字節,來快速地判斷該文件是否有可
能是RDB 文件。
RDB-VERSION
一個四字節長的以字符表示的整數,記錄了該文件所使用的RDB 版本號。
目前的RDB 文件版本爲0006 。
由於不一樣版本的RDB 文件互不兼容,因此在讀入程序時,須要根據版原本選擇不一樣的讀入方
式。
DB-DATA
這個部分在一個RDB 文件中會出現任意屢次,每一個DB-DATA 部分保存着服務器上一個非空數
據庫的全部數據。
• rdbSave 會將數據庫數據保存到RDB 文件,並在保存完成以前阻塞調用者。
• SAVE 命令直接調用rdbSave ,阻塞Redis 主進程;BGSAVE 用子進程調用rdbSave ,
主進程仍可繼續處理命令請求。
• SAVE 執行期間,AOF 寫入能夠在後臺線程進行,BGREWRITEAOF 能夠在子進程進
行,因此這三種操做能夠同時進行。
• 爲了不產生競爭條件,BGSAVE 執行時,SAVE 命令不能執行。
• 爲了不性能問題,BGSAVE 和BGREWRITEAOF 不能同時執行。
• 調用rdbLoad 函數載入RDB 文件時,不能進行任何和數據庫相關的操做,不過訂閱與
發佈方面的命令能夠正常執行,由於它們和數據庫不相關聯。
• RDB 文件的組織方式以下:
• 鍵值對在RDB 文件中的組織方式以下:
RDB 文件使用不一樣的格式來保存不一樣類型的值。
Redis 分別提供了RDB 和AOF 兩種持久化機制:
• RDB 將數據庫的快照(snapshot)以二進制的方式保存到磁盤中。
• AOF 則以協議文本的方式,將全部對數據庫進行過寫入的命令(及其參數)記錄到AOF
文件,以此達到記錄數據庫狀態的目的。
Redis 將全部對數據庫進行過寫入的命令(及其參數)記錄到AOF 文件,以此達到記錄數據庫
狀態的目的,爲了方便起見,咱們稱呼這種記錄過程爲同步
爲了處理的方便,AOF 文件使用網絡通信協議的格式來保存這些命令
緩存追加
當命令被傳播到AOF 程序以後,程序會根據命令以及命令的參數,將命令從字符串對象轉換
回原來的協議文本。
好比說,若是AOF 程序接受到的三個參數分別保存着SET 、KEY 和VALUE 三個字符串,那麼
它將生成協議文本"*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n" 。
協議文本生成以後,它會被追加到redis.h/redisServer 結構的aof_buf 末尾。
redisServer 結構維持着Redis 服務器的狀態,aof_buf 域則保存着全部等待寫入到AOF 文
件的協議文本:
struct redisServer {
// 其餘域...
sds aof_buf;
// 其餘域...
};
至此,追加命令到緩存的步驟執行完畢
文件寫入和保存
每當服務器常規任務函數被執行、或者事件處理器被執行時,aof.c/flushAppendOnlyFile 函
數都會被調用,這個函數執行如下兩個工做:
WRITE:根據條件,將aof_buf 中的緩存寫入到AOF 文件。
SAVE:根據條件,調用fsync 或fdatasync 函數,將AOF 文件保存到磁盤中。
兩個步驟都須要根據必定的條件來執行,而這些條件由AOF 所使用的保存模式來決定,如下
小節就來介紹AOF 所使用的三種保存模式,以及在這些模式下,步驟WRITE 和SAVE 的調
用條件。
AOF 保存模式
Redis 目前支持三種AOF 保存模式,它們分別是:
1. AOF_FSYNC_NO :不保存。
2. AOF_FSYNC_EVERYSEC :每一秒鐘保存一次。
3. AOF_FSYNC_ALWAYS :每執行一個命令保存一次。
如下三個小節將分別討論這三種保存模式。
不保存
在這種模式下,每次調用flushAppendOnlyFile 函數,WRITE 都會被執行,但SAVE 會被
略過。
在這種模式下,SAVE 只會在如下任意一種狀況中被執行:
• Redis 被關閉
• AOF 功能被關閉
• 系統的寫緩存被刷新(多是緩存已經被寫滿,或者按期保存操做被執行)
這三種狀況下的SAVE 操做都會引發Redis 主進程阻塞。
每一秒鐘保存一次
在這種模式中,SAVE 原則上每隔一秒鐘就會執行一次,由於SAVE 操做是由後臺子線程調用
的,因此它不會引發服務器主進程阻塞。
注意,在上一句的說明裏面使用了詞語「原則上」 ,在實際運行中,程序在這種模式下對fsync
或fdatasync 的調用並非每秒一次,它和調用flushAppendOnlyFile 函數時Redis 所處的
狀態有關。
每當flushAppendOnlyFile 函數被調用時,可能會出現如下四種狀況:
• 子線程正在執行SAVE ,而且:
1. 這個SAVE 的執行時間未超過2 秒,那麼程序直接返回,並不執行WRITE 或新的
SAVE 。
2. 這個SAVE 已經執行超過2 秒,那麼程序執行WRITE ,但不執行新的SAVE 。
注意,由於這時WRITE 的寫入必須等待子線程先完成(舊的)SAVE ,所以這裏
WRITE 會比平時阻塞更長時間。
• 子線程沒有在執行SAVE ,而且:
3. 上次成功執行SAVE 距今不超過1 秒,那麼程序執行WRITE ,但不執行SAVE 。
4. 上次成功執行SAVE 距今已經超過1 秒,那麼程序執行WRITE 和SAVE
根據以上說明能夠知道,在「每一秒鐘保存一次」模式下,若是在狀況1 中發生故障停機,那麼
用戶最多損失小於2 秒內所產生的全部數據。
若是在狀況2 中發生故障停機,那麼用戶損失的數據是能夠超過2 秒的。
Redis 官網上所說的,AOF 在「每一秒鐘保存一次」時發生故障,只丟失1 秒鐘數據的說法,實
際上並不許確。
每執行一個命令保存一次
在這種模式下,每次執行完一個命令以後,WRITE 和SAVE 都會被執行。
另外,由於SAVE 是由Redis 主進程執行的,因此在SAVE 執行期間,主進程會被阻塞,不能
接受命令請求。
AOF 保存模式對性能和安全性的影響
在上一個小節,咱們簡短地描述了三種AOF 保存模式的工做方式,如今,是時候研究一下這
三個模式在安全性和性能方面的區別了。
對於三種AOF 保存模式,它們對服務器主進程的阻塞狀況以下:
1. 不保存(AOF_FSYNC_NO):寫入和保存都由主進程執行,兩個操做都會阻塞主進程。
2. 每一秒鐘保存一次(AOF_FSYNC_EVERYSEC):寫入操做由主進程執行,阻塞主進程。保存
操做由子線程執行,不直接阻塞主進程,但保存操做完成的快慢會影響寫入操做的阻塞
時長。
3. 每執行一個命令保存一次(AOF_FSYNC_ALWAYS):和模式1 同樣。
由於阻塞操做會讓Redis 主進程沒法持續處理請求,因此通常說來,阻塞操做執行得越少、完
成得越快,Redis 的性能就越好。
模式1 的保存操做只會在AOF 關閉或Redis 關閉時執行,或者由操做系統觸發,在通常狀況
下,這種模式只須要爲寫入阻塞,所以它的寫入性能要比後面兩種模式要高,固然,這種性能
的提升是以下降安全性爲代價的:在這種模式下,若是運行的中途發生停機,那麼丟失數據的
數量由操做系統的緩存沖洗策略決定。
模式2 在性能方面要優於模式3 ,而且在一般狀況下,這種模式最多丟失很少於2 秒的數據,
因此它的安全性要高於模式1 ,這是一種兼顧性能和安全性的保存方案。
模式3 的安全性是最高的,但性能也是最差的,由於服務器必須阻塞直到命令信息被寫入並保
存到磁盤以後,才能繼續處理請求。
綜合起來,三種AOF 模式的操做特性能夠總結以下:
AOF 文件保存了Redis 的數據庫狀態,而文件裏面包含的都是符合Redis 通信協議格式的命
令文本。
這也就是說,只要根據AOF 文件裏的協議,從新執行一遍裏面指示的全部命令,就能夠還原
Redis 的數據庫狀態了。
Redis 須要對AOF 文件進行重寫(rewrite):建立一個新的AOF 文件
來代替原有的AOF 文件,新AOF 文件和原有AOF 文件保存的數據庫狀態徹底同樣,但新
AOF 文件的體積小於等於原有AOF 文件的體積
所謂的「重寫」實際上是一個有歧義的詞語,實際上,AOF 重寫並不須要對原有的AOF 文件進行
任何寫入和讀取,它針對的是數據庫中鍵的當前值。
根據鍵的類型,使用適當的寫入命令來重現鍵的當前值,這就是AOF 重寫的實現原理。
AOF 後臺重寫
上一節展現的AOF 重寫程序能夠很好地完成建立一個新AOF 文件的任務,可是,在執行這
個程序的時候,調用者線程會被阻塞。
很明顯,做爲一種輔佐性的維護手段,Redis 不但願AOF 重寫形成服務器沒法處理請求,因此
Redis 決定將AOF 重寫程序放到(後臺)子進程裏執行,這樣處理的最大好處是:
1. 子進程進行AOF 重寫期間,主進程能夠繼續處理命令請求。
2. 子進程帶有主進程的數據副本,使用子進程而不是線程,能夠在避免鎖的狀況下,保證數
據的安全性。
不過,使用子進程也有一個問題須要解決:由於子進程在進行AOF 重寫期間,主進程還須要
繼續處理命令,而新的命令可能對現有的數據進行修改,這會讓當前數據庫的數據和重寫後的
AOF 文件中的數據不一致。
爲了解決這個問題,Redis 增長了一個AOF 重寫緩存,這個緩存在fork 出子進程以後開始啓
用,Redis 主進程在接到新的寫命令以後,除了會將這個寫命令的協議內容追加到現有的AOF
文件以外,還會追加到這個緩存中
AOF 後臺重寫的觸發條件
AOF 重寫能夠由用戶經過調用BGREWRITEAOF 手動觸發。
另外,服務器在AOF 功能開啓的狀況下,會維持如下三個變量:
• 記錄當前AOF 文件大小的變量aof_current_size 。
• 記錄最後一次AOF 重寫以後,AOF 文件大小的變量aof_rewirte_base_size 。
• 增加百分比變量aof_rewirte_perc 。
每次當serverCron 函數執行時,它都會檢查如下條件是否所有知足,若是是的話,就會觸發
自動的AOF 重寫:
1. 沒有BGSAVE 命令在進行。
2. 沒有BGREWRITEAOF 在進行。
3. 當前AOF 文件大小大於server.aof_rewrite_min_size (默認值爲1 MB)。
4. 當前AOF 文件大小和最後一次AOF 重寫後的大小之間的比率大於等於指定的增加百分
比。
默認狀況下,增加百分比爲100% ,也便是說,若是前面三個條件都已經知足,而且當前AOF
文件大小比最後一次AOF 重寫時的大小要大一倍的話,那麼觸發自動AOF 重寫。
• AOF 文件經過保存全部修改數據庫的命令來記錄數據庫的狀態。
• AOF 文件中的全部命令都以Redis 通信協議的格式保存。
• 不一樣的AOF 保存模式對數據的安全性、以及Redis 的性能有很大的影響。
• AOF 重寫的目的是用更小的體積來保存數據庫狀態,整個重寫過程基本上不影響Redis
主進程處理命令請求。
• AOF 重寫是一個有歧義的名字,實際的重寫工做是針對數據庫的當前值來進行的,程序
既不讀寫、也不使用原有的AOF 文件。
• AOF 能夠由用戶手動觸發,也能夠由服務器自動觸發。
事件是Redis 服務器的核心,它處理兩項重要的任務:
1. 處理文件事件:在多個客戶端中實現多路複用,接受它們發來的命令請求,並將命令的執
行結果返回給客戶端。
2. 時間事件:實現服務器常規操做(server cron job)。
Redis 將這類由於對套接字進行多路複用而產生的事件稱爲文件事件(file event),文件事件可
以分爲讀事件和寫事件兩類
讀事件
讀事件標誌着客戶端命令請求的發送狀態。
當一個新的客戶端鏈接到服務器時,服務器會給爲該客戶端綁定讀事件,直到客戶端斷開鏈接
以後,這個讀事件纔會被移除。
寫事件
寫事件標誌着客戶端對命令結果的接收狀態。
和客戶端自始至終都關聯着讀事件不一樣,服務器只會在有命令結果要傳回給客戶端時,纔會爲
客戶端關聯寫事件,而且在命令結果傳送完畢以後,客戶端和寫事件的關聯就會被移除
由於在同一次文件事件處理器的調用中,單個客戶端只能執行其中一種事件(要麼讀,要麼寫,
但不能又讀又寫),當出現讀事件和寫事件同時就緒的狀況時,事件處理器優先處理讀事件。
這也就是說,當服務器有命令結果要返回客戶端,而客戶端又有新命令請求進入時,服務器先
處理新命令請求。
時間事件記錄着那些要在指定時間點運行的事件,多個時間事件以無序鏈表的形式保存在服務
器狀態中。
每一個時間事件主要由三個屬性組成:
• when :以毫秒格式的UNIX 時間戳爲單位,記錄了應該在什麼時間點執行事件處理函數。
• timeProc :事件處理函數。
• next 指向下一個時間事件,造成鏈表。
根據timeProc 函數的返回值,能夠將時間事件劃分爲兩類:
• 若是事件處理函數返回ae.h/AE_NOMORE ,那麼這個事件爲單次執行事件:該事件會在指
定的時間被處理一次,以後該事件就會被刪除,再也不執行。
• 若是事件處理函數返回一個非AE_NOMORE 的整數值,那麼這個事件爲循環執行事件:該
事件會在指定的時間被處理,以後它會按照事件處理函數的返回值,更新事件的when 屬
性,讓這個事件在以後的某個時間點再次運行,並以這種方式一直更新並運行下去。
能夠用僞代碼來表示這兩種事件的處理方式:
def handle_time_event(server, time_event):
# 執行事件處理器,並獲取返回值
# 返回值能夠是AE_NOMORE ,或者一個表示毫秒數的非符整數值
retval = time_event.timeProc()
if retval == AE_NOMORE:
# 若是返回AE_NOMORE ,那麼將事件從鏈表中刪除,再也不執行
server.time_event_linked_list.delete(time_event)
else:
# 不然,更新事件的when 屬性
# 讓它在當前時間以後的retval 毫秒以後再次運行
time_event.when = unix_ts_in_ms() + retval
當時間事件處理器被執行時,它遍歷全部鏈表中的時間事件,檢查它們的到達事件(when 屬
性),並執行其中的已到達事件:
def process_time_event(server):
# 遍歷時間事件鏈表
for time_event in server.time_event_linked_list:
# 檢查事件是否已經到達
if time_event.when >= unix_ts_in_ms():
# 處理已到達事件
handle_time_event(server, time_event)
Note: 無序鏈表並不影響時間事件處理器的性能
在目前的版本中,正常模式下的Redis 只帶有serverCron 一個時間事件,而在benchmark 模
式下,Redis 也只使用兩個時間事件。
在這種狀況下,程序幾乎是將無序鏈表退化成一個指針來使用,因此使用無序鏈表來保存時間
事件,並不影響事件處理器的性能。
時間事件應用實例:服務器常規操做
對於持續運行的服務器來講,服務器須要按期對自身的資源和狀態進行必要的檢查和整理,從
而讓服務器維持在一個健康穩定的狀態,這類操做被統稱爲常規操做(cron job)。
在Redis 中,常規操做由redis.c/serverCron 實現,它主要執行如下操做:
• 更新服務器的各種統計信息,好比時間、內存佔用、數據庫佔用狀況等。
• 清理數據庫中的過時鍵值對。
• 對不合理的數據庫進行大小調整。
• 關閉和清理鏈接失效的客戶端。
• 嘗試進行AOF 或RDB 持久化操做。
• 若是服務器是主節點的話,對附屬節點進行按期同步。
• 若是處於集羣模式的話,對集羣進行按期同步和鏈接測試。
Redis 將serverCron 做爲時間事件來運行,從而確保它每隔一段時間就會自動運行一次,又
由於serverCron 須要在Redis 服務器運行期間一直按期運行,因此它是一個循環時間事件:
serverCron 會一直按期執行,直到服務器關閉爲止。
在Redis 2.6 版本中,程序規定serverCron 每隔10 毫秒就會被運行一次。從Redis 2.8 開始,
10 毫秒是serverCron 運行的默認間隔,而具體的間隔能夠由用戶本身調整
實際處理時間事件的時間,一般會比時間事件所預約的時間要晚,至於延遲的
時間有多長,取決於時間事件執行以前,執行文件事件所消耗的時間
文件事件先於時間事件處理,根據狀況,若是處理文件事件耗費了很是多的時間,serverCron 被推遲到一兩秒以後才能執行,也是有可能的。
• Redis 的事件分爲時間事件和文件事件兩類。
• 文件事件分爲讀事件和寫事件兩類:讀事件實現了命令請求的接收,寫事件實現了命令
結果的返回。
• 時間事件分爲單次執行事件和循環執行事件,服務器常規操做serverCron 就是循環事
件。
• 文件事件和時間事件之間是合做關係:一種事件會等待另外一種事件完成以後再執行,不
會出現搶佔狀況。
• 時間事件的實際執行時間一般會比預約時間晚一些。
從啓動Redis 服務器,到服務器能夠接受外來客戶端的網絡鏈接這段時間,Redis 須要執行一
系列初始化操做。
整個初始化過程能夠分爲如下六個步驟:
1. 初始化服務器全局狀態。
2. 載入配置文件。
3. 建立daemon 進程。
4. 初始化服務器功能模塊。
5. 載入數據。
6. 開始事件循環。
1. 初始化服務器全局狀態
redis.h/redisServer 結構記錄了和服務器相關的全部數據,這個結構主要包含如下信息:
• 服務器中的全部數據庫。
• 命令表:在執行命令時,根據字符來查找相應命令的實現函數。
• 事件狀態。
• 服務器的網絡鏈接信息:套接字地址、端口,以及套接字描述符。
• 全部已鏈接客戶端的信息。
• Lua 腳本的運行環境及相關選項。
• 實現訂閱與發佈(pub/sub)功能所需的數據結構。
• 日誌(log)和慢查詢日誌(slowlog)的選項和相關信息。
• 數據持久化(AOF 和RDB)的配置和狀態。
• 服務器配置選項:好比要建立多少個數據庫,是否將服務器進程做爲daemon 進程來運
行,最大鏈接多少個客戶端,壓縮結構(zip structure)的實體數量,等等。
• 統計信息:好比鍵有多少次命令、不命中,服務器的運行時間,內存佔用,等等。
在這一步,程序建立一個redisServer 結構的實例變量server 用做服務器的全局狀態,並將
server 的各個屬性初始化爲默認值。
2. 載入配置文件
在初始化服務器的上一步中,程序爲server 變量(也便是服務器狀態)的各個屬性設置了默
認值,但這些默認值有時候並非最合適的:
• 用戶可能想使用AOF 持久化,而不是默認的RDB 持久化。
• 用戶可能想用其餘端口來運行Redis ,以免端口衝突。
• 用戶可能不想使用默認的16 個數據庫,而是分配更多或更少數量的數據庫。
• 用戶可能想對默認的內存限制措施和回收策略作調整。
等等。
爲了讓使用者按本身的要求配置服務器,Redis 容許用戶在運行服務器時,提供相應的配置文
件(config file)或者顯式的選項(option),Redis 在初始化完server 變量以後,會讀入配置
文件和選項,而後根據這些配置來對server 變量的屬性值作相應的修改:
1. 若是單純執行redis-server 命令,那麼服務器以默認的配置來運行Redis 。
2. 另外一方面,若是給Redis 服務器送入一個配置文件,那麼Redis 將按配置文件的設置來
更新服務器的狀態。
好比說,經過命令redis-server /etc/my-redis.conf ,Redis 會根據my-redis.conf
文件的內容來對服務器狀態作相應的修改。
3. 除此以外,還能夠顯式地給服務器傳入選項,直接修改服務器配置。
舉個例子,經過命令redis-server --port 10086 ,可讓Redis 服務器端口變動爲
10086 。
4. 固然,同時使用配置文件和顯式選項也是能夠的,若是文件和選項有衝突的地方,那麼優
先使用選項所指定的配置值。
舉個例子,若是運行命令redis-server /etc/my-redis.conf --port 10086 ,而且
my-redis.conf 也指定了port 選項,那麼服務器將優先使用--port 10086 (其實是
選項指定的值覆蓋了配置文件中的值)
3. 建立daemon 進程
Redis 默認以daemon 進程的方式運行。
當服務器初始化進行到這一步時,程序將建立daemon 進程來運行Redis ,並建立相應的pid
文件。
4. 初始化服務器功能模塊
在這一步,初始化程序完成兩件事:
• 爲server 變量的數據結構子屬性分配內存。
• 初始化這些數據結構。
爲數據結構分配內存,並初始化這些數據結構,等同於對相應的功能進行初始化。
好比說,當爲訂閱與發佈所需的鏈表分配內存以後,訂閱與發佈功能就處於就緒狀態,隨時可
覺得Redis 所用了。
在這一步,程序完成的主要動做以下:
• 初始化Redis 進程的信號功能。
• 初始化日誌功能。
• 初始化客戶端功能。
• 初始化共享對象。
• 初始化事件功能。
• 初始化數據庫。
• 初始化網絡鏈接。
• 初始化訂閱與發佈功能。
• 初始化各個統計變量。
• 關聯服務器常規操做(cron job)到時間事件,關聯客戶端應答處理器到文件事件。
• 若是AOF 功能已打開,那麼打開或建立AOF 文件。
• 設置內存限制。
• 初始化Lua 腳本環境。
• 初始化慢查詢功能。
• 初始化後臺操做線程
雖然全部功能已經就緒,但這時服務器的數據庫仍是一片空白,程序還須要將服務器上一次執
行時記錄的數據載入到當前服務器中,服務器的初始化纔算真正完成。
5.載入數據
在這一步,程序須要將持久化在RDB 或者AOF 文件裏的數據,載入到服務器進程裏面。
若是服務器有啓用AOF 功能的話,那麼使用AOF 文件來還原數據;不然,程序使用RDB 文
件來還原數據。
當執行完這一步時,服務器打印出一段載入完成信息:
[6717] 22 Feb 11:59:14.830 * DB loaded from disk: 0.068 seconds
6. 開始事件循環
到了這一步,服務器的初始化已經完成,程序打開事件循環,開始接受客戶端鏈接。
如下是服務器在這一步打印的信息:
[6717] 22 Feb 11:59:14.830 * The server is now ready to accept connections on port 6379
如下是初始化完成以後,服務器狀態和各個模塊之間的關係圖:
Redis 以多路複用的方式來處理多個客戶端,爲了讓多個客戶端之間獨立分開、不互相干擾,
服務器爲每一個已鏈接客戶端維持一個redisClient 結構,從而單獨保存該客戶端的狀態信息。
redisClient 結構主要包含如下信息:
• 套接字描述符。
• 客戶端正在使用的數據庫指針和數據庫號碼。
• 客戶端的查詢緩存(query buffer)和回覆緩存(reply buffer)。
• 一個指向命令函數的指針,以及字符串形式的命令、命令參數和命令個數,這些屬性會在
命令執行時使用。
• 客戶端狀態:記錄了客戶端是否處於SLAVE 、MONITOR 或者事務狀態。
• 實現事務功能(好比MULTI 和WATCH)所需的數據結構。
• 實現阻塞功能(好比BLPOP 和BRPOPLPUSH)所需的數據結構。
• 實現訂閱與發佈功能(好比PUBLISH 和SUBSCRIBE)所需的數據結構。
• 統計數據和選項:客戶端建立的時間,客戶端和服務器最後交互的時間,緩存的大小,等
等。
命令的請求、處理和結果返回
當客戶端連上服務器以後,客戶端就能夠向服務器發送命令請求了。
從客戶端發送命令請求,到命令被服務器處理、並將結果返回客戶端,整個過程有如下步驟:
1. 客戶端經過套接字向服務器傳送命令協議數據。
2. 服務器經過讀事件來處理傳入數據,並將數據保存在客戶端對應redisClient 結構的查
詢緩存中。
3. 根據客戶端查詢緩存中的內容,程序從命令表中查找相應命令的實現函數。
4. 程序執行命令的實現函數,修改服務器的全局狀態server 變量,並將命令的執行結果保
存到客戶端redisClient 結構的回覆緩存中,而後爲該客戶端的fd 關聯寫事件。
5. 當客戶端fd 的寫事件就緒時,將回復緩存中的命令結果傳回給客戶端。至此,命令執行
完畢。
命令請求實例:SET 的執行過程
爲了更直觀地理解命令執行的整個過程,咱們用一個實際執行SET 命令的例子來說解命令執
行的過程。
假設如今客戶端C1 是鏈接到服務器S 的一個客戶端,當用戶執行命令SET YEAR 2013 時,客
戶端調用寫入函數,將協議內容*3\r\n$3\r\nSET\r\n$4\r\nYEAR\r\n$4\r\n2013\r\n" 寫
入鏈接到服務器的套接字中。
當S 的文件事件處理器執行時,它會察覺到C1 所對應的讀事件已經就緒,因而它將協議文本
讀入,並保存在查詢緩存。
經過對查詢緩存進行分析(parse),服務器在命令表中查找SET 字符串所對應的命令實現函數,
最終定位到t_string.c/setCommand 函數,另外,兩個命令參數YEAR 和2013 也會以字符串
的形式保存在客戶端結構中。
接着,程序將客戶端、要執行的命令、命令參數等送入命令執行器:執行器調用setCommand
函數,將數據庫中YEAR 鍵的值修改成2013 ,而後將命令的執行結果保存在客戶端的回覆緩存
中,併爲客戶端fd 關聯寫事件,用於將結果回寫給客戶端。
由於YEAR 鍵的修改,其餘和數據庫命名空間相關程序,好比AOF 、REPLICATION 還有事
務安全性檢查(是否修改了被WATCH 監視的鍵?)也會被觸發,當這些後續程序也執行完畢之
後,命令執行器退出,服務器其餘程序(好比時間事件處理器)繼續運行。
當C1 對應的寫事件就緒時,程序就會將保存在客戶端結構回覆緩存中的數據回寫給客戶端,
當客戶端接收到數據以後,它就將結果打印出來,顯示給用戶看。
以上就是SET YEAR 2013 命令執行的整個過程
• 服務器通過初始化以後,才能開始接受命令。
• 服務器初始化能夠分爲六個步驟:
1. 初始化服務器全局狀態。
2. 載入配置文件。
3. 建立daemon 進程。
4. 初始化服務器功能模塊。
5. 載入數據。
6. 開始事件循環。
• 服務器爲每一個已鏈接的客戶端維持一個客戶端結構,這個結構保存了這個客戶端的全部
狀態信息。
• 客戶端向服務器發送命令,服務器接受命令而後將命令傳給命令執行器,執行器執行給
定命令的實現函數,執行完成以後,將結果保存在緩存,最後回傳給客戶端。
2.爲了防止A意外釋放B的鎖,val的值能夠設置成該機器的惟一標識,例如時間+請求號。在解鎖時須要校驗是否是解鎖的請求來自於同一個服務器,若是不是說明這是別人的鎖不能解