Redis 深入之道
前言
在 Redis 系列的開篇文章中,我們對 Redis 概述以及 Redis 數(shù)據(jù)結構與對象進行了詳細的討論以及了解。經(jīng)過上一篇文章的閱讀,相信讀者已經(jīng)對 Redis 的內部結構有了大致了解,接下來我們繼續(xù)深入了解 Redis 內部結構。
對于 Redis,相信大家對 “Redis 的持久化有哪幾種方式?”、“Redis 的數(shù)據(jù)淘汰機制?” 、“Redis 的過期鍵淘汰策略?” 、“Redis 訂閱與發(fā)布機制?”、“Redis 主從復制的原理?” 等面試題目都不陌生,那么本文就從常見的 Redis 面試題目出發(fā),帶領大家深入了解 Redis。
Redis 數(shù)據(jù)庫結構?
服務器中的數(shù)據(jù)庫
Redis 服務器將所有數(shù)據(jù)庫都保存在服務器狀態(tài) redis.h/redisServer 結構的 db 數(shù)組中,db 數(shù)據(jù)的每一項都是一個 redis.h/redisDb 結構,每個 redisDb 結構代表一個數(shù)據(jù)庫:
struct?redisServer?{
????//?...
????//?服務器的數(shù)據(jù)庫數(shù)量
????int?dbnum;
????//?一個數(shù)組,保存著服務器中的所有數(shù)據(jù)庫
????redisDb?*db;
????//?...
};
在初始化服務器時,程序會根據(jù)服務器狀態(tài)的 dbnum 屬性來決定應該創(chuàng)建多少個數(shù)據(jù)庫,dbnum 屬性的值由服務器配置的 database 選項決定,默認情況下,該選項的值為 16。默認情況下,Redis 客戶端的目標數(shù)據(jù)庫為 0 號數(shù)據(jù)庫。
在服務器內部,客戶端狀態(tài) redisClient 結構的 db 屬性記錄了客戶端當前的目標數(shù)據(jù)庫,這個屬性是一個指向 redisDb 結構的指針:
struct?redisClient?{
????//?...
????//?記錄客戶端當前正在使用的數(shù)據(jù)庫
????redisDb?*db;
????//?...
}?redisClient;

客戶端通過修改目標數(shù)據(jù)庫指針,讓它指向 redisServer.db 數(shù)組中的不同元素來切換不同的數(shù)據(jù)庫。
數(shù)據(jù)庫鍵空間
Redis 是一個鍵值對(key-value pair)數(shù)據(jù)庫服務器,服務器中的每個數(shù)據(jù)庫都由一個 redis.h/redisDb 結構表示,數(shù)據(jù)庫主要由 dict 和 expires 兩個字典構成,其中,redisDb 結構的 dict 字典保存了數(shù)據(jù)庫中的所有鍵值對,我們將這個字典稱為鍵空間(key space);而 expires 字典保存了數(shù)據(jù)庫中的鍵的過期時間:
typedef?struct?reddisDb?{
????//?...
????//?數(shù)據(jù)庫鍵空間,保存著數(shù)據(jù)庫中的所有鍵值對
????dict?*dict;
????//?過期時間,保存著鍵的過期時間
????dict?*expires;
????//?...
}?redisDb;

因為數(shù)據(jù)庫的鍵空間是一個字典,所以所有針對數(shù)據(jù)庫的操作,比如添加一個鍵值對到數(shù)據(jù)庫,或者從數(shù)據(jù)庫中刪除一個鍵值對,又或者在數(shù)據(jù)庫中獲取某個鍵值對等,實際上都是通過鍵空間字段進行操作來實現(xiàn)的。
當使用 Redis 命令對數(shù)據(jù)庫進行讀寫時,服務器不僅會對鍵空間執(zhí)行指定的讀寫操作,還會執(zhí)行一些額外的維護操作,其中包括:
在讀取一個鍵之后(讀操作和寫操作都要對鍵進行讀取),服務器會根據(jù)鍵是否存在來更新服務器的鍵空間命中(hit)次數(shù)或者鍵空間不命中(miss)次數(shù),這兩個值可以在 INFO stats 命令的 keyspace_hits 屬性和 keyspace_misses 屬性中查看。
在讀取一個鍵之后,服務器會更新鍵的 LRU(最后一次使用)時間,這個值可以用于計算鍵的閑置時間,使用 OBJECT idletime <key> 命令可以查看鍵 key 的閑置時間。
如果服務器在讀取一個鍵時發(fā)現(xiàn)該鍵已經(jīng)過期,那么服務器會先刪除這個過期鍵,然后才執(zhí)行余下的其它操作。
如果有客戶端使用 WATCH 命令監(jiān)視了某個鍵,那么服務器在對被監(jiān)視的鍵進行修改之后,會將這個鍵標記為臟(dirty),從而讓事物程序注意到這個鍵已經(jīng)被修改過。
服務器每次修改一個鍵之后,都會對臟(dirty)鍵計數(shù)器的值增 1,這個計數(shù)器會觸發(fā)服務器的持久化以及復制操作。
如果服務器開啟了數(shù)據(jù)庫通知功能,那么在對鍵進行修改之后,服務器將按配置發(fā)送相應的數(shù)據(jù)庫通知。
Redis 的過期鍵淘汰策略?
過期時間
設置鍵的生存時間或者過期時間
通過 EXPIRE 命令或者 PEXPIRE 命令,客戶端可以以秒或者毫秒精度為數(shù)據(jù)庫中的某個鍵設置生存時間(Time To Live,TTL),在經(jīng)過指定的秒數(shù)或者毫秒數(shù)之后,服務器就會自動刪除生存時間為 0 的鍵。
與 EXPIRE 命令和 PEXPIRE 命令類似,客戶端可以通過 EXPIREAT 命令或者 PEXPIREAT 命令,以秒或者毫秒精度給數(shù)據(jù)庫中的某個鍵設置過期時間(expire time)。
雖然有多種不同單位和不同形式的設置命令,但實際上 EXPIRE、PEXPIRE、EXPIREAT 三個命令都是使用 PEXPIREAT 命令來實現(xiàn)的:無論客戶端執(zhí)行的是以上四個命令中的哪一個,經(jīng)過轉換之后,最終的執(zhí)行效果都和執(zhí)行 PEXPIREAT 命令一樣。
保存過期時間
redisDb 結構的 expires 字典保存了數(shù)據(jù)中所有鍵的過期時間,我們稱這個字典為過期字典:
過期字典的鍵是一個指針,這個指針指向鍵空間中的某個鍵對象(也即是某個數(shù)據(jù)庫鍵)。
過期字典的值是一個 long 類型的整數(shù),這個整數(shù)保存了鍵所指向的數(shù)據(jù)庫鍵的過期時間——一個毫秒精度的 UNIX 時間戳。
typedef?struct?redisDb?{
????//?...
????//?數(shù)據(jù)庫鍵空間,保存著數(shù)據(jù)庫中的所有鍵值對
????dict?*dict;
????//?過期時間,保存著鍵的過期時間
????dict?*expires;
????//?...
}?redisDb;

過期鍵刪除策略
定時刪除
在設置鍵的過期時間的同時,創(chuàng)建一個定時器(timer),讓定時器在鍵的過期時間來臨時,立即執(zhí)行對鍵的刪除操作。(主動刪除)
優(yōu)點:對內存是最友好的,通過使用定時器,定時刪除策略可以保證過期鍵會盡可能快地被刪除,并釋放過期鍵所占用的內存。
缺點:對 CPU 時間是最不友好的,在過期鍵比較多的情況下,刪除過期鍵這一行為可能會占用相當一部分的 CPU 時間,在內存不緊張但是 CPU 時間非常緊張的情況下,將 CPU 時間用在刪除和當前任務無關的過期鍵上,無疑會對服務器的相應時間和吞吐量造成影響。
惰性刪除
放任鍵過期不管,但是每次從鍵空間中獲取鍵時,都檢查取得的鍵是否過期,如果過期的話,就刪除該鍵;如果沒有過期,就返回該鍵。(被動刪除)
優(yōu)點:對 CPU 時間來說是最友好的,程序只會在取出鍵時才對鍵進行過期檢查,這可以保證刪除過期鍵的操作只會在非做不可的情況下進行,并且刪除的目標僅限于當前處理的鍵,這個策略不會在刪除其他無關的過期鍵上花費任何 CPU 時間。
缺點:對內存不友好,如果一個鍵已經(jīng)過期,而這個鍵又仍然保留在數(shù)據(jù)庫中,那么只要這個過期不被刪除,它所占用的內存就不會釋放。
定期刪除
每隔一段時間,程序就對數(shù)據(jù)庫進行一次檢查,刪除里面的過期鍵。至于要刪除多少過期鍵,以及要檢查多少個數(shù)據(jù)庫,則由算法決定。(主動刪除)
優(yōu)點:定期刪除策略是定時刪除和惰性刪除兩種策略的一種整合和折中,通過限制刪除操作執(zhí)行的時長和頻率來減少刪除操作對 CPU 時間的影響;通過定期刪除過期鍵,有效減少了因為過期鍵而帶來的內存浪費。
缺點:定期刪除策略的難點是確定刪除操作執(zhí)行的時長和頻率,如果刪除操作執(zhí)行得太頻繁或者執(zhí)行的時間太長,定期刪除策略就會退化成定時刪除策略,以至于將 CPU 時間過多地消耗在刪除過期鍵上面;如果刪除操作執(zhí)行的得太少或者執(zhí)行得時間太短,定期刪除策略又會和惰性刪除策略一樣,出現(xiàn)浪費內存的情況。
Redis 的過期鍵刪除策略
Redis 服務器實際使用的是惰性刪除和定期刪除兩種策略(定期刪除是集中處理,惰性刪除是零散處理):通過配合使用這兩種刪除策略,服務器可以很好地合理使用 CPU 時間和避免浪費內存空間之間取得平衡
惰性刪除策略的實現(xiàn):過期鍵的惰性刪除策略由 db.c/expireIfNeeded 函數(shù)實現(xiàn),所有讀寫數(shù)據(jù)庫的 Redis 命令在執(zhí)行之前都會調用 expireIfNeeded 函數(shù)對輸入鍵進行檢查。expireIfNeeded 函數(shù)就像一個過濾器,它可以在命令真正執(zhí)行之前,過濾掉過期的輸入鍵,從而避免命令接觸到過期鍵。

定期刪除策略的實現(xiàn):過期鍵的定期刪除策略由 redis.c/activeExpireCycle 函數(shù)實現(xiàn),每當 Redis 的服務器周期性操作 redis.c/serverCron 函數(shù)執(zhí)行時,activeExpireCycle 函數(shù)就會被調用,它在規(guī)定的時間內,分多次遍歷服務器中的各個數(shù)據(jù)庫(默認每次檢查的數(shù)據(jù)庫數(shù)量為 16),從數(shù)據(jù)庫的 expire 字典中隨機檢查一部分鍵(默認每個數(shù)據(jù)庫檢查的鍵數(shù)量為 20)的過期時間,并刪除其中的過期鍵。
AOF、RDB 和復制功能對過期鍵的處理
執(zhí)行 SAVE 命令或者 BGSAVE 命令所產(chǎn)生的新 RDB 文件不會包含已經(jīng)過期的鍵。
執(zhí)行 BGREWRITEAOF 命令所產(chǎn)生的重寫 AOF 文件不會包含已經(jīng)過期的鍵。
當一個過期鍵被刪除之后,服務器會追加一條 DEL 命令到現(xiàn)有 AOF 文件的末尾,顯式地刪除過期鍵。
當主服務器刪除一個過期鍵之后,它會向所有從服務器發(fā)送一條 DEL 命令,顯式地刪除過期鍵。
從服務器即使發(fā)現(xiàn)過期鍵也不會自作主張地刪除它,而是等待主節(jié)點發(fā)來 DEL 命令,這種統(tǒng)一、中心化的過期鍵刪除策略可以保證主從服務器數(shù)據(jù)的一致性。
當 Redis 命令對數(shù)據(jù)庫進行修改之后,服務器會根據(jù)配置向客戶端發(fā)送數(shù)據(jù)庫通知。
Redis 的數(shù)據(jù)淘汰機制?
Redis 配置文件中可以使用 maxmemory<bytes> 將內存使用限制設置為指定的字節(jié)數(shù)。當達到內存限制時,Redis 會根據(jù)選擇的淘汰策略來刪除鍵。這樣可以減少內存緊張的情況,由此獲取更為穩(wěn)健的服務。Redis 內存數(shù)據(jù)集大小上升到一定大小的時候,就會施行數(shù)據(jù)淘汰策略。
Redis 中當內存超過限制時,按照配置的策略,淘汰掉相應的 kv,使得內存可以繼續(xù)留有足夠的空間保存新的數(shù)據(jù)。Redis 確定驅逐某個鍵值對后,會刪除這個數(shù)據(jù),并將這個數(shù)據(jù)變更消息發(fā)布到本地(AOF 持久化)和從機(主從連接)。
緩存淘汰算法
FIFO(First In First Out,先進先出算法) 一種比較容易實現(xiàn)的算法。它的思想是先進先出(FIFO,隊列),這是最簡單、最公平的一種思想,即如果一個數(shù)據(jù)是最先進入的,那么可以認為在將來它被訪問的可能性很小。空間滿的時候,最先進入的數(shù)據(jù)最先被置換(淘汰)。
LRU(Least Recently Used, 最近最少使用算法 )是一種常見的緩存算法,在很多分布式緩存系統(tǒng)(如 Redis、Memcached)中都有廣泛使用。LRU 算法的思想是:如果一個數(shù)據(jù)在最近一段時間沒有被訪問到,那么可以認為在將來它被訪問的可能性也很小。因此,當空間滿時,最久沒有訪問的數(shù)據(jù)最先被置換(淘汰)。
LFU(Least Frequently Used , 最不經(jīng)常使用算法)也是一種常見的緩存算法。LFU 算法的思想是:如果一個數(shù)據(jù)在最近一段時間很少被訪問到,那么可以認為在將來它被訪問的可能性也很小。因此,當空間滿時,最小頻率訪問的數(shù)據(jù)最先被置換(淘汰)。
Redis 提供 6 種數(shù)據(jù)淘汰策略
我們在該系列的上一篇文章中了解到,redisobject 中除了 type、encoding、ptr 和 refcount 屬性外,還有一個 lru 屬性用來計算空轉時長。OBJECT IDLETIME 命令可以打印出給定鍵的空轉時長,是用當前時間減去鍵的 lru 時間計算得出的。OBJECT IDLETIME 命令是特殊的,這個命令在訪問鍵的對象時,不會修改值對象的 lru 屬性。
鍵的空轉時長還有一個作用,如果服務器打開了 maxmemory 選項,并且服務器用于回收內存的算法是 volatile-lru 或者 allkeys-lru,那么當服務器占用的內存數(shù)超過了 maxmemory 選項所設置的上限值時,空轉時長較高的那部分鍵會優(yōu)先被服務器釋放,從而回收內存。
volatile-lru : 從已設置過期時間的數(shù)據(jù)集 (server.db[i].expires) 中挑選最近最少使用的數(shù)據(jù)淘汰。(推薦)
volatile-ttl : 從已設置過期時間的數(shù)據(jù)集 (server.db[i].expires) 中挑選將要過期的數(shù)據(jù)淘汰。
volatile-random : 從已設置過期時間的數(shù)據(jù)集 (server.db[i].expires) 中任意選擇數(shù)據(jù)淘汰。
allkeys-lru : 從數(shù)據(jù)集 (server.db[i].dict) 中挑選最近最少使用的數(shù)據(jù)淘汰。(一般推薦)
allkeys-random : 從數(shù)據(jù)集 (server.db[i].dict) 中任意選擇數(shù)據(jù)淘汰。
no-enviction:不會繼續(xù)服務寫請求 (DEL 請求可以繼續(xù)服務),讀請求可以繼續(xù)進行。這樣可以保證不會丟失數(shù)據(jù),但是會讓線上的業(yè)務不能持續(xù)進行。(默認)
Redis 4.0 版本后增加以下兩種:
volatile-lfu:從已設置過期時間的數(shù)據(jù)集 (server.db[i].expires) 中挑選最不經(jīng)常使用的數(shù)據(jù)淘汰。
allkeys-lfu:從數(shù)據(jù)集 (server.db[i].dict) 中挑選最不經(jīng)常使用的數(shù)據(jù)淘汰。
在 Redis 中 LRU 算法是一個近似算法,默認情況下,Redis 隨機挑選 5 個鍵,并且從中選取一個最近最久未使用的 key 進行淘汰,在配置文件中可以通過 maxmemory-samples 的值來設置 redis 需要檢查 key 的個數(shù),但是檢查的越多,耗費的時間也就越久,結構越精確 (也就是 Redis 從內存中淘汰的對象未使用的時間也就越久),設置多少,綜合權衡。
對于具體的數(shù)據(jù)淘汰機制以及數(shù)據(jù)淘汰策略,大家可以閱讀?Redis 配置文件 redis.conf?中有相關注釋。
Redis 的持久化有哪幾種方式?
因為 Redis 是內存數(shù)據(jù)庫,它將自己的數(shù)據(jù)庫狀態(tài)儲存在內存里面,所以如果不想辦法將儲存在內存的數(shù)據(jù)庫狀態(tài)保存至磁盤里面,那么一旦服務器進程退出,服務器中的數(shù)據(jù)庫狀態(tài)也會消失不見。
為了解決這個問題,Redis 提供了 RDB(Redis DataBase) 持久化功能,這個功能可以將 Redis 在內存中的數(shù)據(jù)庫狀態(tài)保存到磁盤里面,避免數(shù)據(jù)意外丟失。
除了 RDB 持久化功能之外,Redis 還提供了 AOF(Append Only File)持久化功能。與 RDB 持久化通過保存數(shù)據(jù)庫中的鍵值對來記錄數(shù)據(jù)庫狀態(tài)不同,AOF 持久化是通過保存 Redis 服務器所執(zhí)行的寫命令來記錄數(shù)據(jù)庫狀態(tài)的。
RDB(Redis DataBase)
RDB(Redis DataBase) 是 Redis 默認的持久化方案。在指定的時間間隔內,執(zhí)行指定次數(shù)的寫操作,則會將內存中的數(shù)據(jù)寫入到磁盤中。即在指定目錄下生成一個 dump.rdb 文件,Redis 重啟會通過加載 dump.rdb 文件恢復數(shù)據(jù)。
RDB 文件是一個經(jīng)過壓縮的二進制文件,由多個部分組成,用于保存和還原 Redis 服務器所有數(shù)據(jù)庫中的所有鍵值對數(shù)據(jù)。對于不同類型的鍵值對,RDB 文件會使用不同的方式來保存它們。

RDB 文件的創(chuàng)建和載入
有兩個 Redis 命令可以用于生成 RDB 文件,一個是 SAVE,另一個是 BGSAVE。生成操作會耗費服務器大量的 CPU、內存和磁盤 I/O 資源。
SAVE 命令有服務器進程直接執(zhí)行保存操作,因此 SAVE 命令會阻塞 Redis 服務器進程,直到 RDB 文件創(chuàng)建完畢為止,在服務器進程阻塞期間,服務器不能處理任何命令請求。
BGSAVE 命令由子進程執(zhí)行保存操作,BGSAVE 命令會派生(fork)出一個子進程,然后由子進程負責創(chuàng)建 RDB 文件,服務器進程(父進程)繼續(xù)處理命令請求,所以該命令不會阻塞服務器。
服務器在載入 RDB 文件期間,會一直處于阻塞狀態(tài),直到載入工作完成為止。

自動間隔性保存
當 Redis 服務器啟動時,用戶可以通過指定配置文件或者傳入啟動參數(shù)的方式設置 save 選項,如果用戶沒有主動設置 save 選項,那么服務器就會為 save 選項設置默認條件:
//?服務器在 900?秒(15 分鐘)之內,對數(shù)據(jù)庫進行了至少 1 次修改。
save?900?1
//?服務器在 300?秒(5 分鐘)之內,對數(shù)據(jù)庫進行了至少 10?次修改。
save?300?10
//?服務器在 60?秒(1 分鐘)之內,對數(shù)據(jù)庫進行了至少 10000?次修改。
save?60?10000
以上三個條件中的任意一個滿足,BGSAVE 命令就會被執(zhí)行。Redis 的服務器周期性操作函數(shù) serverCron 默認每隔 100 毫秒就會執(zhí)行一次,該函數(shù)用于對正在運行的服務器進行維護,它的其中一項工作就是檢查 save 選項所設置的保存條件是否已經(jīng)滿足,如果滿足的話,就執(zhí)行 BGSAVE 命令。
AOF(Append Only File)
AOF(Append Only File)在 Redis 中默認不開啟(appendonly no), 默認是每秒將寫操作日志追加到 AOF 文件中,它的出現(xiàn)是為了彌補 RDB 的不足(數(shù)據(jù)的不一致性),所以它采用日志的形式來記錄每個寫操作,并追加到文件中。Redis 重啟的時候會根據(jù)日志文件 appendonly.aof 的內容將寫指令從前到后執(zhí)行一次以完成數(shù)據(jù)的恢復工作。
AOF 文件中的所有命令都以 Redis 命令請求協(xié)議的格式保存,請求命令會先保存到 AOF 緩沖區(qū)里面,之后再定期寫入并同步到 AOF 文件。
如果服務器開啟了 AOF 持久化功能,那么服務器會優(yōu)先使用 AOF 文件來還原數(shù)據(jù)庫狀態(tài)。只有在 AOF 持久化功能處于關閉狀態(tài)時,服務器才會使用 RDB 文件來還原數(shù)據(jù)庫狀態(tài)。

AOF 文件的載入與數(shù)據(jù)還原
因為 AOF 文件里面包含了重建數(shù)據(jù)庫狀態(tài)所需的所有寫命令,所以服務器只要讀入并重新執(zhí)行一遍 AOF 文件里面保存的寫命令,就可以還原服務器關閉之前的數(shù)據(jù)庫狀態(tài)。

AOF 重寫
因為 AOF 持久化是通過保存被執(zhí)行的寫命令來記錄數(shù)據(jù)庫狀態(tài)的,所以隨著服務器運行時間的流逝,AOF 文件的體積也會越來越大,如果不加以控制的話,體積過大的 AOF 文件很可能對 Redis 服務器、甚至整個宿主計算機造成影響,并且 AOF 文件的體積越大,使用 AOF 文件來進行數(shù)據(jù)還原所需的時間就越多。
為了解決 AOF 文件體積膨脹的問題,Redis 提供了 AOF 文件重寫(rewrite)功能。AOF 重寫是一個有歧義的名字,該功能是通過讀取數(shù)據(jù)庫中的鍵值對來實現(xiàn)的,程序無須對現(xiàn)有的 AOF 文件進行任何讀取、分析或者寫入操作。通過該功能,Redis 服務器可以創(chuàng)建一個新的 AOF 文件來替代現(xiàn)有的 AOF 文件,新舊兩個 AOF 文件所保存的數(shù)據(jù)庫狀態(tài)相同,但新的 AOF 文件不會包含任何浪費空間的冗余命令,所以新的 AOF 文件的體積通常會比舊的 AOF 文件體積要小得多。
在執(zhí)行 BGREWRITEAOF 命令時,Redis 服務器會維護一個 AOF 重寫緩沖區(qū),該緩沖區(qū)會在子進程創(chuàng)建新 AOF 文件期間,記錄服務器執(zhí)行的所有寫命令。當子進程完成創(chuàng)建新 AOF 文件的工作之后,服務器會將重寫緩沖區(qū)中的所有內容追加到新的 AOF 文件的末尾,使得新舊兩個 AOF 文件所保存的數(shù)據(jù)庫狀態(tài)一致。最后,服務器用新的 AOF 文件替換舊的 AOF 文件,以此來完成 AOF 文件重寫操作。
Redis 4.0 對于持久化機制的優(yōu)化
Redis 4.0 開始支持 RDB 和 AOF 的混合持久化(默認關閉,可以通過配置項 aof-use-rdb-preamble 開啟)。如果把混合持久化打開,AOF 重寫的時候就直接把 RDB 的內容寫到 AOF 文件開頭。這樣做的好處是可以結合 RDB 和 AOF 的優(yōu)點,快速加載同時避免丟失過多的數(shù)據(jù)。當然缺點也是有的, AOF 里面的 RDB 部分是壓縮格式不再是 AOF 格式,可讀性較差。

Redis 訂閱(subscribe)與發(fā)布(publish)機制?
Redis 的發(fā)布和訂閱功能由 PUBLIST、SUBSCRIBE、PSUBSCRIBE 等命令組成。通過執(zhí)行 SUBSCRIBE 命令,客戶端可以訂閱一個或多個頻道,從而你成為這些頻道的訂閱者(subscriber):每當有其它客戶端向被訂閱的頻道發(fā)送消息(message)時,頻道的所有訂閱者都會收到這條消息。除了訂閱頻道之外,客戶端還可以通過執(zhí)行 PSUBSCRIBE 命令訂閱一個或多個模式,從而成為這些模式的訂閱者:每當有其它客戶端向某個頻道發(fā)送消息時,消息不僅會被發(fā)送給這個頻道的所有訂閱者,它還會被發(fā)送給所有與這個頻道相匹配的模式的訂閱者。

服務器狀態(tài)在 pubsub_channels 字典保存了所有頻道的訂閱關系, 字典的鍵為被訂閱的頻道,字典的值為訂閱頻道的所有客戶端:SUBSCRIBE 命令負責將客戶端和被訂閱的頻道關聯(lián)到這個字典里面,而 UNSUBSCRIBE 命令則負責解除客戶端和被退訂頻道之間的關聯(lián)。當有新消息發(fā)送到頻道時,程序遍歷頻道(鍵)所對應的(值)所有客戶端,然后將消息發(fā)送到所有訂閱頻道的客戶端上。
服務器狀態(tài)在 pubsub_patterns 鏈表保存了所有模式的訂閱關系,鏈表的每個節(jié)點都保存著一個 pubsubPattern 結構,結構中保存著被訂閱的模式,以及訂閱該模式的客戶端:PSBUSCRIBE 命令負責將客戶端和被訂閱的模式記錄到這個鏈表中,而 PUNSUBSCRIBE 命令則負責移除客戶端和被退訂模式在鏈表中的記錄。程序通過遍歷鏈表來查找某個頻道是否和某個模式匹配。
PUBLISH 命令通過訪問 pubsub_channels 字典在向頻道的所有訂閱者發(fā)送消息,通過訪問 pubsub_patterns 鏈表來向所有匹配頻道的模式的訂閱者發(fā)送消息。
PUBSUB 命令的三個子命令都是通過讀取 pubsub_channels 字典和 pubsub_patterns 鏈表中的信息來實現(xiàn)的。
struct?redisServer?{
????//?...
????//?保存所有頻道訂閱關系
????dict?*pubsub_channels;
????//?保存所有模式訂閱關系
????list?*pubsub_patterns;
????//?...
};
Redis 主從復制的原理?
在 Redis 中,用戶可以通過執(zhí)行 SLAVEOF 命令或者設置 slaveof 選項,讓一個服務器去復制(replicate)另一個服務器,我們稱呼被復制的服務器為主服務器(master),而對主服務器進行復制的服務器則稱為從服務器(slave)。進行復制的主從服務器雙方的數(shù)據(jù)庫將保存相同的數(shù)據(jù),概念上將這種現(xiàn)象稱作 “數(shù)據(jù)庫狀態(tài)一致”,或者簡稱 “一致”。
復制功能的實現(xiàn)
Redis 的復制功能分為同步(sync)和命令傳播(command propagate)兩個操作:
同步:同步操作用于將服務器的數(shù)據(jù)庫狀態(tài)更新至主服務器當前所處的數(shù)據(jù)庫狀態(tài)。
命令傳播:命令傳播操作則用于在主服務器的數(shù)據(jù)庫狀態(tài)被修改,導致主從服務器的數(shù)據(jù)庫狀態(tài)出現(xiàn)不一致時,讓主從服務器的數(shù)據(jù)庫重新回到一致狀態(tài)。
從服務器對主服務器的同步操作需要通過向主服務器發(fā)送 SYNC 命令來完成,以下是 Redis 復制功能的執(zhí)行步驟:
1) 從服務器向主服務器發(fā)送 SYNC 命令。
2) 收到 SYNC 命令的主服務器執(zhí)行 BGSAVE 命令,在后臺生成一個 RDB 文件,并使用一個緩沖區(qū)記錄從現(xiàn)在開始執(zhí)行的所有寫命令。
3) 當主服務器的 BGSAVE 命令執(zhí)行完畢時,主服務器會將 BGSAVE 命令生成的 RDB 文件發(fā)送給從服務器,從服務器接收并載入這個 RDB 文件,將自己的數(shù)據(jù)庫狀態(tài)更新至主服務器執(zhí)行 BGSAVE 命令時的數(shù)據(jù)庫狀態(tài)。
4) 主服務器將記錄在緩沖區(qū)里面的所有寫命令發(fā)給從服務器,從服務器執(zhí)行這些寫命令,將自己的數(shù)據(jù)庫狀態(tài)更新至主服務器數(shù)據(jù)庫當前所處的狀態(tài)。
5) 主服務器將自己執(zhí)行的寫命令,也即是造成主從服務器不一致的那條寫命令,發(fā)給從服務器執(zhí)行,當從服務器執(zhí)行了相同的寫命令之后,主從服務器將再次回到一致狀態(tài)。(命令傳播)
部分重同步的實現(xiàn)
在 Redis 中,從服務器對主服務器的復制可以分為以下兩種情況:
初次復制:從服務器以前沒有復制過任何主服務器,或者從服務器當前要復制的主服務器和上一次復制的主服務器不同。
斷線后重復制:處于命令傳播階段的主從服務器因為網(wǎng)絡原因而中斷了復制,但從服務器通過自動重連接重新連上了主服務器,并繼續(xù)復制主從服務器。
Redis 2.8 以前的復制功能不能高效地處理斷線后重復值情況,但 Redis 2.8 新添加的部分重同步功能可以解決這個問題。部分重同步通過復制偏移量、復制積壓緩沖區(qū)、服務器運行 ID 三個部分來實現(xiàn)。
在復制操作剛開始的時候,從服務器會成為主服務器的客戶端,并通過向主服務器發(fā)送命令請求來執(zhí)行復制步驟,而在復制操作的后期,主從服務器會相互成為對方的客戶端(正因為主服務器成為了從服務器的客戶端,所以主服務器才可以發(fā)送寫命令來改變從服務器的數(shù)據(jù)庫狀態(tài))。
心跳檢測
主服務器通過向從服務器傳播命令來更新從服務器的狀態(tài),保持主從服務器一致,而從服務器則通過向主服務器發(fā)送命令進行心跳檢測(默認以每秒一次的頻率),以及命令丟失檢測。
參考博文
[1].?《Redis 設計與實現(xiàn)》,第二部分 單機數(shù)據(jù)庫的實現(xiàn)
[2].?分布式之數(shù)據(jù)庫和緩存雙寫一致性方案解析
[3].?Redis 配置文件 redis.conf
source:https://blog.maoning.vip/archives/e993c76c.html
喜歡,在看
