Redis 深度歷險: 核心原理和應用實踐2

目錄

1.應用 9:大海撈針 —— Scan 

原理 1:鞭辟入裏 —— 線程 IO 模型 

原理 2:交頭接耳 —— 通信協議

原理 3:未雨綢繆 —— 持久化 

原理 4:雷厲風行 —— 管道 

原理 5:同舟共濟 —— 事務 

爲什麼 Redis 不支持回滾 roll back

原理 6:小道消息 —— PubSub

原理 7:開源節流 —— 小對象壓縮 

原理 8:有備無患 —— 主從同步 


1.應用 9:大海撈針 —— Scan 

在平時線上 Redis 維護工作中,有時候需要從 Redis 實例成千上萬的 key 中找出特定
前綴的 key 列表來手動處理數據,可能是修改它的值,也可能是刪除 key。這裏就有一個問
題,如何從海量的 key 中找出滿足特定前綴的 key 列表來

Redis 提供了一個簡單暴力的指令 keys 用來列出所有滿足特定正則字符串規則的 key

這個指令使用非常簡單,提供一個簡單的正則字符串即可,但是有很明顯的兩個缺點。 
1、沒有 offset、limit 參數,一次性吐出所有滿足條件的 key,萬一實例中有幾百 w 個 
key 滿足條件,當你看到滿屏的字符串刷的沒有盡頭時,你就知道難受了

2、keys 算法是遍歷算法,複雜度是 O(n),如果實例中有千萬級以上的 key,這個指令
就會導致 Redis 服務卡頓,所有讀寫 Redis 的其它的指令都會被延後甚至會超時報錯,因爲 
Redis 是單線程程序,順序執行所有指令,其它指令必須等到當前的 keys 指令執行完了才
可以繼續。

Redis 爲了解決這個問題,它在 2.8 版本中加入了大海撈針的指令——scan。scan 相比 
keys 具備有以下特點: 

1、複雜度雖然也是 O(n),但是它是通過遊標分步進行的,不會阻塞線程; 
2、提供 limit 參數,可以控制每次返回結果的最大條數,limit 只是一個 hint,返回的
結果可多可少; 
3、同 keys 一樣,它也提供模式匹配功能; 
4、服務器不需要爲遊標保存狀態,遊標的唯一狀態就是 scan 返回給客戶端的遊標整數; 
5、返回的結果可能會有重複,需要客戶端去重複,這點非常重要; 
6、遍歷的過程中如果有數據修改,改動後的數據能不能遍歷到是不確定的; 
7、單次返回的結果是空的並不意味着遍歷結束,而要看返回的遊標值是否爲零; 

scan 基礎使用 

scan 參數提供了三個參數,第一個是 cursor 整數值,第二個是 key 的正則模式,第三
個是遍歷的 limit hint。第一次遍歷時,cursor 值爲 0,然後將返回結果中第一個整數值作爲
下一次遍歷的 cursor。一直遍歷到返回的 cursor 值爲 0 時結束

從上面的過程可以看到雖然提供的 limit 是 1000,但是返回的結果只有 10 個左右。因
爲這個 limit 不是限定返回結果的數量,而是限定服務器單次遍歷的字典槽位數量(約等於)。

如果將 limit 設置爲 10,你會發現返回結果是空的,但是遊標值不爲零,意味着遍歷還沒結
束。

SCAN命令的有SCAN,SSCAN,HSCAN,ZSCAN。
SCAN的話就是遍歷所有的keys
其他的SCAN命令的話是SCAN選中的集合。
SCAN命令是增量的循環,每次調用只會返回一小部分的元素。所以不會有KEYS命令的坑。
SCAN命令返回的是一個遊標,從0開始遍歷,到0結束遍歷。
 

字典的結構 

在 Redis 中所有的 key 都存儲在一個很大的字典中,這個字典的結構和 Java 中的 
HashMap 一樣,是一維數組 + 二維鏈表結構,第一維數組的大小總是 2^n(n>=0),擴容一
次數組大小空間加倍,也就是 n++

scan 指令返回的遊標就是第一維數組的位置索引,我們將這個位置索引稱爲槽 (slot)。
如果不考慮字典的擴容縮容,直接按數組下標挨個遍歷就行了。limit 參數就表示需要遍歷的
槽位數,之所以返回的結果可能多可能少,是因爲不是所有的槽位上都會掛接鏈表,有些槽
位可能是空的,還有些槽位上掛接的鏈表上的元素可能會有多個。每一次遍歷都會將 limit 
數量的槽位上掛接的所有鏈表元素進行模式匹配過濾後,一次性返回給客戶端

scan 遍歷順序

scan 的遍歷順序非常特別。它不是從第一維數組的第 0 位一直遍歷到末尾,而是採用
了高位進位加法來遍歷。之所以使用這樣特殊的方式進行遍歷,是考慮到字典的擴容和縮容
時避免槽位的遍歷重複和遺漏
普通加法和高位進位加法的區別 
高位進位法從左邊加,進位往右邊移動,同普通加法正好相反。但是最終它們都會遍歷
所有的槽位並且沒有重複。

字典擴容 

Java 中的 HashMap 有擴容的概念,當 loadFactor 達到閾值時,需要重新分配一個新的 
2 倍大小的數組,然後將所有的元素全部 rehash 掛到新的數組下面。rehash 就是將元素的 
hash 值對數組長度進行取模運算,因爲長度變了,所以每個元素掛接的槽位可能也發生了變
化。又因爲數組的長度是 2^n 次方,所以取模運算等價於位與操作

漸進式 rehash 
Java 的 HashMap 在擴容時會一次性將舊數組下掛接的元素全部轉移到新數組下面。如
果 HashMap 中元素特別多,線程就會出現卡頓現象。Redis 爲了解決這個問題,它採用漸
進式 rehash

它會同時保留舊數組和新數組,然後在定時任務中以及後續對 hash 的指令操作中漸漸
地將舊數組中掛接的元素遷移到新數組上。這意味着要操作處於 rehash 中的字典,需要同
時訪問新舊兩個數組結構。如果在舊數組下面找不到元素,還需要去新數組下面去尋找。 
scan 也需要考慮這個問題,對與 rehash 中的字典,它需要同時掃描新舊槽位,然後將
結果融合後返回給客戶端。 

更多的 scan 指令 

scan 指令是一系列指令,除了可以遍歷所有的 key 之外,還可以對指定的容器集合進
行遍歷。比如 zscan 遍歷 zset 集合元素,hscan 遍歷 hash 字典的元素、sscan 遍歷 set 集
合的元素。 
它們的原理同 scan 都會類似的,因爲 hash 底層就是字典,set 也是一個特殊的 
hash(所有的 value 指向同一個元素),zset 內部也使用了字典來存儲所有的元素內容,所以
這裏不再贅述。

大 key 掃描 

有時候會因爲業務人員使用不當,在 Redis 實例中會形成很大的對象,比如一個很大的 
hash,一個很大的 zset 這都是經常出現的。這樣的對象對 Redis 的集羣數據遷移帶來了很
大的問題,因爲在集羣環境下,如果某個 key 太大,會數據導致遷移卡頓。另外在內存分配
上,如果一個 key 太大,那麼當它需要擴容時,會一次性申請更大的一塊內存,這也會導致
卡頓。如果這個大 key 被刪除,內存會一次性回收,卡頓現象會再一次產生。 
 
在平時的業務開發中,要儘量避免大 key 的產生。 
如果你觀察到 Redis 的內存大起大落,這極有可能是因爲大 key 導致的,這時候你就
需要定位出具體是那個 key,進一步定位出具體的業務來源,然後再改進相關業務代碼設
計。 

那如何定位大 key 呢?
爲了避免對線上 Redis 帶來卡頓,這就要用到 scan 指令,對於掃描出來的每一個 
key,使用 type 指令獲得 key 的類型,然後使用相應數據結構的 size 或者 len 方法來得到
它的大小,對於每一種類型,保留大小的前 N 名作爲掃描結果展示出來。 
上面這樣的過程需要編寫腳本,比較繁瑣,不過 Redis 官方已經在 redis-cli 指令中提供
了這樣的掃描功能,我們可以直接拿來即用。 
redis-cli -h 127.0.0.1 -p 7001 –-bigkeys 
如果你擔心這個指令會大幅擡升 Redis 的 ops 導致線上報警,還可以增加一個休眠參
數。 
redis-cli -h 127.0.0.1 -p 7001 –-bigkeys -i 0.1 
上面這個指令每隔 100 條 scan 指令就會休眠 0.1s,ops 就不會劇烈擡升,但是掃描的
時間會變長

原理 1:鞭辟入裏 —— 線程 IO 模型 

  Redis 是個單線程程序!這點必須銘記。 
也許你會懷疑高併發的 Redis 中間件怎麼可能是單線程。很抱歉,它就是單線程,你的
懷疑暴露了你基礎知識的不足。莫要瞧不起單線程,除了 Redis 之外,Node.js 也是單線
程,Nginx 也是單線程,但是它們都是服務器高性能的典範

原理 2:交頭接耳 —— 通信協議

Redis 將所有數據都放在內存,用一個單線程對外提供服務,單個節點在跑滿一個 CPU 核心的情
況下可以達到了 10w/s 的超高 QPS。 

以上2個原理不知道講的啥,看不懂!!!

原理 3:未雨綢繆 —— 持久化 


 Redis 的數據全部在內存裏,如果突然宕機,數據就會全部丟失,因此必須有一種機制
來保證 Redis 的數據不會因爲故障而丟失,這種機制就是 Redis 的持久化機制。 
Redis 的持久化機制有兩種,第一種是快照,第二種是 AOF 日誌。快照是一次全量備
份,AOF 日誌是連續的增量備份。快照是內存數據的二進制序列化形式,在存儲上非常

湊,而 AOF 日誌記錄的是內存數據修改的指令記錄文本。AOF 日誌在長期的運行過程中會
變的無比龐大,數據庫重啓時需要加載 AOF 日誌進行指令重放,這個時間就會無比漫長。
所以需要定期進行 AOF 重寫,給 AOF 日誌進行瘦身。



快照原理 

我們知道 Redis 是單線程程序,這個線程要同時負責多個客戶端套接字的併發讀寫操作
和內存數據結構的邏輯讀寫。 
在服務線上請求的同時,Redis 還需要進行內存快照,內存快照要求 Redis 必須進行文
件 IO 操作,可文件 IO 操作是不能使用多路複用 API。 
這意味着單線程同時在服務線上的請求還要進行文件 IO 操作,文件 IO 操作會嚴重拖
垮服務器請求的性能。還有個重要的問題是爲了不阻塞線上的業務,就需要邊持久化邊響應
客戶端請求。持久化的同時,內存數據結構還在改變,比如一個大型的 hash 字典正在持久
化,結果一個請求過來把它給刪掉了,還沒持久化完呢,這尼瑪要怎麼搞? 
那該怎麼辦呢? 
Redis 使用操作系統的多進程 COW(Copy On Write) 機制來實現快照持久化,這個機制
很有意思,也很少人知道。多進程 COW 也是鑑定程序員知識廣度的一個重要指標

fork(多進程)  

Redis 在持久化時會調用 glibc 的函數 fork 產生一個子進程,快照持久化完全交給子進
程來處理,父進程繼續處理客戶端請求。子進程剛剛產生時,它和父進程共享內存裏面的代
碼段和數據段。這時你可以將父子進程想像成一個連體嬰兒,共享身體。這是 Linux 操作系
統的機制,爲了節約內存資源,所以儘可能讓它們共享起來。在進程分離的一瞬間,內存的
增長几乎沒有明顯變化

子進程做數據持久化,它不會修改現有的內存數據結構,它只是對數據結構進行遍歷讀
取,然後序列化寫到磁盤中。但是父進程不一樣,它必須持續服務客戶端請求,然後對內存
數據結構進行不間斷的修改。 
這個時候就會使用操作系統的 COW 機制來進行數據段頁面的分離。數據段是由很多操
作系統的頁面組合而成,當父進程對其中一個頁面的數據進行修改時,會將被共享的頁面復
制一份分離出來,然後對這個複製的頁面進行修改。這時子進程相應的頁面是沒有變化的,
還是進程產生時那一瞬間的數據子進程因爲數據沒有變化,它能看到的內存裏的數據在進程產生的一瞬間就凝固了,再
也不會改變,這也是爲什麼 Redis 的持久化叫「快照」的原因。接下來子進程就可以非常安
心的遍歷數據了進行序列化寫磁盤了。 

AOF 原理

AOF 日誌存儲的是 Redis 服務器的順序指令序列,AOF 日誌只記錄對內存進行修改的
指令記錄

假設 AOF 日誌記錄了自 Redis 實例創建以來所有的修改性指令序列,那麼就可以通過
對一個空的 Redis 實例順序執行所有的指令,也就是「重放」,來恢復 Redis 當前實例的內
存數據結構的狀態

Redis 會在收到客戶端修改指令後,先進行參數校驗,如果沒問題,就立即將該指令文
本存儲到 AOF 日誌中,也就是先存到磁盤,然後再執行指令。這樣即使遇到突發宕機,已
經存儲到 AOF 日誌的指令進行重放一下就可以恢復到宕機前的狀態。 
Redis 在長期運行的過程中,AOF 的日誌會越變越長。如果實例宕機重啓,重放整個 
AOF 日誌會非常耗時,導致長時間 Redis 無法對外提供服務。所以需要對 AOF 日誌瘦

AOF 重寫 

Redis 提供了 bgrewriteaof 指令用於對 AOF 日誌進行瘦身。其原理就是開闢一個子進
程對內存進行遍歷轉換成一系列 Redis 的操作指令,序列化到一個新的 AOF 日誌文件中。
序列化完畢後再將操作期間發生的增量 AOF 日誌追加到這個新的 AOF 日誌文件中,追加
完畢後就立即替代舊的 AOF 日誌文件了,瘦身工作就完成了

fsync

AOF 日誌是以文件的形式存在的,當程序對 AOF 日誌文件進行寫操作時,實際上是將
內容寫到了內核爲文件描述符分配的一個內存緩存中,然後內核會異步將髒數據刷回到磁盤
的。 
這就意味着如果機器突然宕機,AOF 日誌內容可能還沒有來得及完全刷到磁盤中,這個
時候就會出現日誌丟失。那該怎麼辦? 
Linux 的 glibc 提供了 fsync(int fd)函數可以將指定文件的內容強制從內核緩存刷到磁
盤。只要 Redis 進程實時調用 fsync 函數就可以保證 aof 日誌不丟失。但是 fsync 是一個
磁盤 IO 操作,它很慢!如果 Redis 執行一條指令就要 fsync 一次,那麼 Redis 高性能的
地位就不保了。 
所以在生產環境的服務器中,Redis 通常是每隔 1s 左右執行一次 fsync 操作,週期 1s 
是可以配置的。這是在數據安全性和性能之間做了一個折中,在保持高性能的同時,儘可能
使得數據少丟失

Redis 同樣也提供了另外兩種策略,一個是永不 fsync——讓操作系統來決定合適同步磁
盤,很不安全,另一個是來一個指令就 fsync 一次——非常慢。但是在生產環境基本不會使
用,瞭解一下即可

運維

快照是通過開啓子進程的方式進行的,它是一個比較耗資源的操作。 
    1、遍歷整個內存,大塊寫磁盤會加重系統負載 
    2、AOF 的 fsync 是一個耗時的 IO 操作,它會降低 Redis 性能,同時也會增加系
統 IO 負擔 

所以通常 Redis 的主節點是不會進行持久化操作,持久化操作主要在從節點進行。從節
點是備份節點,沒有來自客戶端請求的壓力,它的操作系統資源往往比較充沛 ,

但是如果出現網絡分區,從節點長期連不上主節點,就會出現數據不一致的問題,特別
是在網絡分區出現的情況下又不小心主節點宕機了,那麼數據就會丟失,所以在生產環境要
做好實時監控工作,保證網絡暢通或者能快速修復。另外還應該再增加一個從節點以降低網
絡分區的概率,只要有一個從節點數據同步正常,數據也就不會輕易丟失。

 Redis 4.0 混合持久化 

重啓 Redis 時,我們很少使用 rdb 來恢復內存狀態,因爲會丟失大量數據。我們通常
使用 AOF 日誌重放,但是重放 AOF 日誌性能相對 rdb 來說要慢很多,這樣在 Redis 實
例很大的情況下,啓動需要花費很長的時間。 
Redis 4.0 爲了解決這個問題,帶來了一個新的持久化選項——混合持久化。將 rdb 文
件的內容和增量的 AOF 日誌文件存在一起。這裏的 AOF 日誌不再是全量的日誌,而是自
持久化開始到持久化結束的這段時間發生的增量 AOF 日誌,通常這部分 AOF 日誌很小

於是在 Redis 重啓的時候,可以先加載 rdb 的內容,然後再重放增量 AOF 日誌就可
以完全替代之前的 AOF 全量文件重放,重啓效率因此大幅得到提升。

原理 4:雷厲風行 —— 管道 

Redis 的消息交互 

兩個連續的寫操作和兩個連續的讀操作總共只會花費一次網絡來回,就好比連續的 write 
操作合併了,連續的 read 操作也合併了一樣
這便是管道操作的本質,服務器根本沒有任何區別對待,還是收到一條消息,執行一條
消息,回覆一條消息的正常的流程。客戶端通過對管道中的指令列表改變讀寫順序就可以大
幅節省 IO 時間。管道中指令越多,效果越好

管道壓力測試

接下來我們實踐一下管道的力量。 
Redis 自帶了一個壓力測試工具 redis-benchmark,使用這個工具就可以進行管道測試

深入理解管道本質 

如上圖

小結 
這就是管道的本質了,它並不是服務器的什麼特性,而是客戶端通過改變了讀寫的順序
帶來的性能的巨大提升

原理 5:同舟共濟 —— 事務 

爲了確保連續多個操作的原子性,一個成熟的數據庫通常都會有事務支持,Redis 也不
例外。Redis 的事務使用非常簡單,不同於關係數據庫,我們無須理解那麼多複雜的事務模
型,就可以直接使用。不過也正是因爲這種簡單性,它的事務模型很不嚴格,這要求我們不
能像使用關係數據庫的事務一樣來使用 Redis。

Redis 事務的基本使用

Redis 在形式上看起來也差不多,分別是 multi/exec/discard。multi 指示事務的開始,
exec 指示事務的執行,discard 指示事務的丟棄。 

上面的指令演示了一個完整的事務過程,所有的指令在 exec 之前不執行,而是緩存在
服務器的一個事務隊列中,服務器一旦收到 exec 指令,纔開執行整個事務隊列,執行完畢
後一次性返回所有指令的運行結果。因爲 Redis 的單線程特性,它不用擔心自己在執行隊列
的時候被其它指令打攪,可以保證他們能得到的「原子性」執行

 原子性

事務的原子性是指要麼事務全部成功,要麼全部失敗,那麼 Redis 事務執行是原子性的
麼? 
下面我們來看一個特別的例子

上面的例子是事務執行到中間遇到失敗了,因爲我們不能對一個字符串進行數學運算,
事務在遇到指令執行失敗後,後面的指令還繼續執行,所以 poorman 的值能繼續得到設置。 
到這裏,你應該明白 Redis 的事務根本不能算「原子性」,而僅僅是滿足了事務的「隔
離性」,隔離性中的串行化——當前執行的事務有着不被其它事務打斷的權利

discard(丟棄)

Redis 爲事務提供了一個 discard 指令,用於丟棄事務緩存隊列中的所有指令,在 exec 
執行之前

 優化

上面的 Redis 事務在發送每個指令到事務緩存隊列時都要經過一次網絡讀寫,當一個事
務內部的指令較多時,需要的網絡 IO 時間也會線性增長。所以通常 Redis 的客戶端在執行
事務時都會結合 pipeline 一起使用,這樣可以將多次 IO 操作壓縮爲單次 IO 操作

Watch 
考慮到一個業務場景,Redis 存儲了我們的賬戶餘額數據,它是一個整數。現在有兩個
併發的客戶端要對賬戶餘額進行修改操作,這個修改不是一個簡單的 incrby 指令,而是要對
餘額乘以一個倍數。Redis 可沒有提供 multiplyby 這樣的指令。我們需要先取出餘額然後在
內存裏乘以倍數,再將結果寫回 Redis。 
這就會出現併發問題,因爲有多個客戶端會併發進行操作。我們可以通過 Redis 的分佈
式鎖來避免衝突,這是一個很好的解決方案。分佈式鎖是一種悲觀鎖,那是不是可以使用樂
觀鎖的方式來解決衝突呢?

Redis 提供了這種 watch 的機制,它就是一種樂觀鎖。有了 watch 我們又多了一種可以
用來解決併發修改的方法。 watch 的使用方式如下:

watch 會在事務開始之前盯住 1 個或多個關鍵變量,當事務執行時,也就是服務器收到
了 exec 指令要順序執行緩存的事務隊列時,Redis 會檢查關鍵變量自 watch 之後,是否被
修改了 (包括當前事務所在的客戶端)。如果關鍵變量被人動過了,exec 指令就會返回 null 
回覆告知客戶端事務執行失敗,這個時候客戶端一般會選擇重試

注意事項 
Redis 禁止在 multi 和 exec 之間執行 watch 指令,而必須在 multi 之前做好盯住關鍵
變量,否則會出錯。 

爲什麼 Redis 不支持回滾 roll back

原理 6:小道消息 —— PubSub

前面我們講了 Redis 消息隊列的使用方法,但是沒有提到 Redis 消息隊列的不足之
處,那就是它不支持消息的多播機制

消息多播

消息多播允許生產者生產一次消息,中間件負責將消息複製到多個消息隊列,每個消息
隊列由相應的消費組進行消費。它是分佈式系統常用的一種解耦方式,用於將多個消費組的
邏輯進行拆分。支持了消息多播,多個消費組的邏輯就可以放到不同的子系統中。 
如果是普通的消息隊列,就得將多個不同的消費組邏輯串接起來放在一個子系統中,進
行連續消費。

PubSub

爲了支持消息多播,Redis 不能再依賴於那 5 種基本數據類型了。它單獨使用了一個模
塊來支持消息多播,這個模塊的名字叫着 PubSub,也就是 PublisherSubscriber,發佈者訂閱
者模型

客戶端發起訂閱命令後,Redis 會立即給予一個反饋消息通知訂閱成功。因爲有網絡傳
輸延遲,在 subscribe 命令發出後,需要休眠一會,再通過 get\_message 才能拿到反饋消
息。客戶端接下來執行發佈命令,發佈了一條消息。同樣因爲網絡延遲,在 publish 命令發
出後,需要休眠一會,再通過 get\_message 才能拿到發佈的消息。如果當前沒有消息,
get\_message 會返回空,告知當前沒有消息,所以它不是阻塞的

Redis PubSub 的生產者和消費者是不同的連接,也就是上面這個例子實際上使用了兩個 
Redis 的連接。這是必須的,因爲 Redis 不允許連接在 subscribe 等待消息時還要進行其它
的操作

這個應用看不下去了,當開個眼界吧 

原理 7:開源節流 —— 小對象壓縮 

小對象壓縮存儲 (ziplist)(說的啥玩意,看不懂)

內存回收機制

Redis 並不總是可以將空閒內存立即歸還給操作系統。 
如果當前 Redis 內存有 10G,當你刪除了 1GB 的 key 後,再去觀察內存,你會發現
內存變化不會太大。原因是操作系統回收內存是以頁爲單位,如果這個頁上只要有一個 key 
還在使用,那麼它就不能被回收。Redis 雖然刪除了 1GB 的 key,但是這些 key 分散到了
很多頁面中,每個頁面都還有其它 key 存在,這就導致了內存不會立即被回收

Redis 雖然無法保證立即回收已經刪除的 key 的內存,但是它會重用那些尚未回收的空
閒內存。這就好比電影院裏雖然人走了,但是座位還在,下一波觀衆來了,直接坐就行。而
操作系統回收內存就好比把座位都給搬走了

內存分配算法 

內存分配是一個非常複雜的課題,需要適當的算法劃分內存頁,需要考慮內存碎片,需
要平衡性能和效率。 
Redis 爲了保持自身結構的簡單性,在內存分配這裏直接做了甩手掌櫃,將內存分配的
細節丟給了第三方內存分配庫去實現。目前 Redis 可以使用 jemalloc(facebook) 庫來管理內
存,也可以切換到 tcmalloc(google)。因爲 jemalloc 相比 tcmalloc 的性能要稍好一些,所以
Redis 默認使用了 jemalloc

通過 info memory 指令可以看到 Redis 的 mem_allocator 使用了 jemalloc。

原理 8:有備無患 —— 主從同步 

很多企業都沒有使用到 Redis 的集羣,但是至少都做了主從。有了主從,當 master 掛
掉的時候,運維讓從庫過來接管,服務就可以繼續,否則 master 需要經過數據恢復和重啓
的過程,這就可能會拖很長的時間,影響線上業務的持續服務

在瞭解 Redis 的主從複製之前,讓我們先來理解一下現代分佈式系統的理論基石——
CAP 原理

CAP 原理

CAP 原理就好比分佈式領域的牛頓定律,它是分佈式存儲的理論基石。自打 CAP 的論
文發表之後,分佈式存儲中間件猶如雨後春筍般一個一個湧現出來

 C - Consistent ,一致性 
 A - Availability ,可用性 
 P - Partition tolerance ,分區容忍性 

分佈式系統的節點往往都是分佈在不同的機器上進行網絡隔離開的,這意味着必然會有
網絡斷開的風險,這個網絡斷開的場景的專業詞彙叫着「網絡分區」.在網絡分區發生時,兩個分佈式節點之間無法進行通信,我們對一個節點進行的修改操
作將無法同步到另外一個節點,所以數據的「一致性」將無法滿足,因爲兩個分佈式節點的
數據不再保持一致。除非我們犧牲「可用性」,也就是暫停分佈式節點服務,在網絡分區發
生時,不再提供修改數據的功能,直到網絡狀況完全恢復正常再繼續對外提供服務

一句話概括 CAP 原理就是——網絡分區發生時,一致性和可用性兩難全

 最終一致

Redis 的主從數據是異步同步的,所以分佈式的 Redis 系統並不滿足「一致性」要求。
當客戶端在 Redis 的主節點修改了數據後,立即返回,即使在主從網絡斷開的情況下,主節
點依舊可以正常對外提供修改服務,所以 Redis 滿足「可用性」。 
Redis 保證「最終一致性」,從節點會努力追趕主節點,最終從節點的狀態會和主節點
的狀態將保持一致。如果網絡斷開了,主從節點的數據將會出現大量不一致,一旦網絡恢
復,從節點會採用多種策略努力追趕上落後的數據,繼續盡力保持和主節點一致

主從同步 

Redis 同步支持主從同步和從從同步,從從同步功能是 Redis 後續版本增加的功能,爲
了減輕主庫的同步負擔

增量同步 

Redis 同步的是指令流,主節點會將那些對自己的狀態產生修改性影響的指令記錄在本
地的內存 buffer 中,然後異步將 buffer 中的指令同步到從節點,從節點一邊執行同步的指
令流來達到和主節點一樣的狀態,一遍向主節點反饋自己同步到哪裏了 (偏移量)。Redis 的複製內存 buffer 是一個定長的環形數組,如果數組內容滿了,就會從頭開始覆
蓋前面的內容。 

如果因爲網絡狀況不好,從節點在短時間內無法和主節點進行同步,那麼當網絡狀況恢
復時,Redis 的主節點中那些沒有同步的指令在 buffer 中有可能已經被後續的指令覆蓋掉
了,從節點將無法直接通過指令流來進行同步,這個時候就需要用到更加複雜的同步機制 —
— 快照同步

快照同步 

快照同步是一個非常耗費資源的操作,它首先需要在主庫上進行一次 bgsave 將當前內
存的數據全部快照到磁盤文件中,然後再將快照文件的內容全部傳送到從節點。從節點將快
照文件接受完畢後,立即執行一次全量加載,加載之前先要將當前內存的數據清空。加載完
畢後通知主節點繼續進行增量同步

增加從節點 

當從節點剛剛加入到集羣時,它必須先要進行一次快照同步,同步完成後再繼續進行增
量同步。 

無盤複製

主節點在進行快照同步時,會進行很重的文件 IO 操作,特別是對於非 SSD 磁盤存儲
時,快照會對系統的負載產生較大影響。特別是當系統正在進行 AOF 的 fsync 操作時如果
發生快照,fsync 將會被推遲執行,這就會嚴重影響主節點的服務效率。 
所以從 Redis 2.8.18 版開始支持無盤複製。所謂無盤複製是指主服務器直接通過套接字
將快照內容發送到從節點,生成快照是一個遍歷的過程,主節點會一邊遍歷內存,一遍將序
列化的內容發送到從節點,從節點還是跟之前一樣,先將接收到的內容存儲到磁盤文件中,
再進行一次性加載

Wait 指令

Redis 的複製是異步進行的,wait 指令可以讓異步複製變身同步複製,確保系統的強一
致性 (不嚴格)。wait 指令是 Redis3.0 版本以後纔出現的。

wait 提供兩個參數,第一個參數是從庫的數量 N,第二個參數是時間 t,以毫秒爲單
位。它表示等待 wait 指令之前的所有寫操作同步到 N 個從庫 (也就是確保 N 個從庫的同
步沒有滯後),最多等待時間 t。如果時間 t=0,表示無限等待直到 N 個從庫同步完成達成
一致。 
假設此時出現了網絡分區,wait 指令第二個參數時間 t=0,主從同步無法繼續進行,
wait 指令會永遠阻塞,Redis 服務器將喪失可用性

小結 
主從複製是 Redis 分佈式的基礎,Redis 的高可用離開了主從複製將無從進行。後面的
章節我們會開始講解 Redis 的集羣模式,這幾種集羣模式都依賴於本節所講的主從複製。 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章