「長文前排提醒,收藏向前排提醒,素質三連 (轉發 + 在看 + 留言) 前排提醒!html
Redis 做爲一個開源的,高級的鍵值存儲和一個適用的解決方案,已經愈來愈在構建 「高性能」、「可擴展」 的 Web 應用上發揮着舉足輕重的做用。前端
當今互聯網技術架構中 Redis 已然成爲了應用得最普遍的中間件之一,它也是中高級後端工程 技術面試 中面試官最喜歡問的工程技能之一,不只僅要求着咱們對 基本的使用 進行掌握,更要深層次地理解 Redis 內部實現 的細節原理。node
熟練掌握 Redis,甚至能夠絕不誇張地說已經半隻腳踏入心儀的公司了。下面咱們一塊兒來盤點回顧一下 Redis 的面試經典問題,就不要再被面試官問得 臉都綠了 呀!git
Redis (Remote Dictionary Server) 是一個使用 C 語言 編寫的,開源的 (BSD許可) 高性能 非關係型 (NoSQL) 的 鍵值對數據庫。程序員
Redis 能夠存儲 鍵 和 不一樣類型數據結構值 之間的映射關係。鍵的類型只能是字符串,而值除了支持最 基礎的五種數據類型 外,還支持一些 高級數據類型:github
「必定要說出一些高級數據結構 (固然你本身也要了解.. 下面會說到的別擔憂),這樣面試官的眼睛纔會亮。web
與傳統數據庫不一樣的是 Redis 的數據是 存在內存 中的,因此 讀寫速度 很是 快,所以 Redis 被普遍應用於 緩存 方向,每秒能夠處理超過 10
萬次讀寫操做,是已知性能最快的 Key-Value 數據庫。另外,Redis 也常常用來作 分佈式鎖。面試
除此以外,Redis 支持事務 、持久化、LUA腳本、LRU驅動事件、多種集羣方案。redis
110000
次/s,寫的速度是
81000
次/s。
在平常的 Web 應用對數據庫的訪問中,讀操做的次數遠超寫操做,比例大概在 1:9 到 3:7,因此須要讀的可能性是比寫的可能大得多的。當咱們使用 SQL 語句去數據庫進行讀寫操做時,數據庫就會 去磁盤把對應的數據索引取回來,這是一個相對較慢的過程。算法
若是咱們把數據放在 Redis 中,也就是直接放在內存之中,讓服務端直接去讀取內存中的數據,那麼這樣 速度 明顯就會快上很多 (高性能),而且會 極大減少數據庫的壓力 (特別是在高併發狀況下)。
「記得是 兩個角度 啊.. 高性能 和 高併發..
可是使用內存進行數據存儲開銷也是比較大的,限於成本 的緣由,通常咱們只是使用 Redis 存儲一些 經常使用和主要的數據,好比用戶登陸的信息等。
通常而言在使用 Redis 進行存儲的時候,咱們須要從如下幾個方面來考慮:
在考慮了這些問題以後,若是以爲有必要使用緩存,那麼就使用它!
通常來講有以下幾個問題,回答思路遵守 是什麼 → 爲何 → 怎麼解決:
另外對於 "Redis 掛掉了,請求所有走數據庫" 這樣的狀況,咱們還能夠有以下的思路:
雙寫一致性上圖仍是稍微粗糙了些,你還須要知道兩種方案 (先操做數據庫和先操做緩存) 分別都有什麼優點和對應的問題,這裏不做贅述,能夠參考一下下方的文章,寫得很是詳細。
「
面試前必需要知道的Redis面試題 | Java3y - mp.weixin.qq.com/s/3Fmv7h5p2…
由於 Redis 是基於內存的操做,CPU 不是 Redis 的瓶頸,Redis 的瓶頸最有多是 機器內存的大小 或者 網絡帶寬。既然單線程容易實現,並且 CPU 不會成爲瓶頸,那就瓜熟蒂落地採用單線程的方案了。
「強烈推薦 各位親看一下這篇文章:
爲何 Redis 選擇單線程模型 · Why's THE Design? - draveness.me/whys-the-de…
簡單總結:
首先在 Redis 內部會使用一個 RedisObject 對象來表示全部的 key
和 value
:
其次 Redis 爲了 平衡空間和時間效率,針對 value
的具體類型在底層會採用不一樣的數據結構來實現,下圖展現了他們之間的映射關係:(好像亂糟糟的,但至少能看清楚..)
C 語言使用了一個長度爲 N+1
的字符數組來表示長度爲 N
的字符串,而且字符數組最後一個元素老是 \0
,這種簡單的字符串表示方式 不符合 Redis 對字符串在安全性、效率以及功能方面的要求。
這樣簡單的數據結構可能會形成如下一些問題:
'\0'
可能會被斷定爲提早結束的字符串而識別不了;
若是去看 Redis 的源碼 sds.h/sdshdr
文件,你會看到 SDS 完整的實現細節,這裏簡單來講一下 Redis 如何解決的:
len
和
alloc
檢查空間是否知足修改所需的要求,若是空間不夠的話,SDS 會自動擴展空間,避免了像 C 字符串操做中的覆蓋狀況;
ascii
碼,對於圖片、音頻等信息沒法保存,SDS 是二進制安全的,寫入什麼讀取就是什麼,不作任何過濾和限制;
字典是 Redis 服務器中出現最爲頻繁的複合型數據結構。除了 hash 結構的數據會用到字典外,整個 Redis 數據庫的全部 key
和 value
也組成了一個 全局字典,還有帶過時時間的 key
也是一個字典。(存儲在 RedisDb 數據結構中)
Redis 中的字典至關於 Java 中的 HashMap,內部實現也差很少相似,都是經過 "數組 + 鏈表" 的 鏈地址法 來解決部分 哈希衝突,同時這樣的結構也吸取了兩種不一樣數據結構的優勢。
字典結構內部包含 兩個 hashtable,一般狀況下只有一個 hashtable
有值,可是在字典擴容縮容時,須要分配新的 hashtable
,而後進行 漸進式搬遷 (rehash),這時候兩個 hashtable
分別存儲舊的和新的 hashtable
,待搬遷結束後,舊的將被刪除,新的 hashtable
取而代之。
正常狀況下,當 hash 表中 元素的個數等於第一維數組的長度時,就會開始擴容,擴容的新數組是 原數組大小的 2 倍。不過若是 Redis 正在作 bgsave(持久化命令)
,爲了減小內存也得過多分離,Redis 儘可能不去擴容,可是若是 hash 表很是滿了,達到了第一維數組長度的 5 倍了,這個時候就會 強制擴容。
當 hash 表由於元素逐漸被刪除變得愈來愈稀疏時,Redis 會對 hash 表進行縮容來減小 hash 表的第一維數組空間佔用。所用的條件是 元素個數低於數組長度的 10%,縮容不會考慮 Redis 是否在作 bgsave
。
「這是 Redis 中比較重要的一個數據結構,建議閱讀 以前寫過的文章,裏面詳細介紹了原理和一些細節:
Redis(2)——跳躍表 - www.wmyskxz.com/2020/02/29/…
「建議閱讀 以前的系列文章:
Redis(4)——神奇的HyperLoglog解決統計問題 - www.wmyskxz.com/2020/03/02/…
「建議閱讀 以前的系列文章:
Redis(5)——億級數據過濾和布隆過濾器 - www.wmyskxz.com/2020/03/11/…
「建議閱讀 以前的系列文章:
Redis(6)——GeoHash查找附近的人 - www.wmyskxz.com/2020/03/12/…
這是 Redis 爲了節約內存 而使用的一種數據結構,zset 和 hash 容器對象會在元素個數較少的時候,採用壓縮列表(ziplist)進行存儲。壓縮列表是 一塊連續的內存空間,元素之間緊挨着存儲,沒有任何冗餘空隙。
「由於以前本身也沒有學習過,因此找了一篇比較比如較容易理解的文章:
圖解Redis之數據結構篇——壓縮列表 - mp.weixin.qq.com/s/nba0FUEAV… 這一篇稍微底層稍微硬核一點: www.web-lovers.com/redis-sourc…
Redis 早期版本存儲 list 列表數據結構使用的是壓縮列表 ziplist 和普通的雙向鏈表 linkedlist,也就是說當元素少時使用 ziplist,當元素多時用 linkedlist。但考慮到鏈表的附加空間相對較高,prev
和 next
指針就要佔去 16
個字節(64 位操做系統佔用 8
個字節),另外每一個節點的內存都是單獨分配,會傢俱內存的碎片化,影響內存管理效率。
後來 Redis 新版本(3.2)對列表數據結構進行了改造,使用 quicklist
代替了 ziplist
和 linkedlist
。
「同上..建議閱讀一下如下的文章:
Redis列表list 底層原理 - zhuanlan.zhihu.com/p/102422311
Redis Stream 從概念上來講,就像是一個 僅追加內容 的 消息鏈表,把全部加入的消息都一個一個串起來,每一個消息都有一個惟一的 ID 和內容,這很簡單,讓它複雜的是從 Kafka 借鑑的另外一種概念:消費者組(Consumer Group) (思路一致,實現不一樣):
上圖就展現了一個典型的 Stream 結構。每一個 Stream 都有惟一的名稱,它就是 Redis 的 key
,在咱們首次使用 xadd
指令追加消息時自動建立。咱們對圖中的一些概念作一下解釋:
XREAD
命令進行
獨立消費,也能夠多個消費者同時加入一個消費者組進行
組內消費。同一個消費者組內的消費者共享全部的 Stream 信息,
同一條消息只會有一個消費者消費到,這樣就能夠應用在分佈式的應用場景中來保證消息的惟一性。
XGROUP CREATE
指令來顯式建立,而且須要指定從哪個消息 ID 開始消費,用來初始化
last_delivered_id
這個變量。
很容易想到,要是消息積累太多,Stream 的鏈表豈不是很長,內容會不會爆掉就是個問題了。xdel
指令又不會刪除消息,它只是給消息作了個標誌位。
Redis 天然考慮到了這一點,因此它提供了一個定長 Stream 功能。在 xadd
的指令提供一個定長長度 maxlen
,就能夠將老的消息幹掉,確保最多不超過指定長度,使用起來也很簡單:
> XADD mystream MAXLEN 2 * value 1
1526654998691-0
> XADD mystream MAXLEN 2 * value 2
1526654999635-0
> XADD mystream MAXLEN 2 * value 3
1526655000369-0
> XLEN mystream
(integer) 2
> XRANGE mystream - +
1) 1) 1526654999635-0
2) 1) "value"
2) "2"
2) 1) 1526655000369-0
2) 1) "value"
2) "3"
複製代碼
若是使用 MAXLEN
選項,當 Stream 的達到指定長度後,老的消息會自動被淘汰掉,所以 Stream 的大小是恆定的。目前尚未選項讓 Stream 只保留給定數量的條目,由於爲了一致地運行,這樣的命令必須在很長一段時間內阻塞以淘汰消息。(例如在添加數據的高峯期間,你不得不長暫停來淘汰舊消息和添加新的消息)
另外使用 MAXLEN
選項的花銷是很大的,Stream 爲了節省內存空間,採用了一種特殊的結構表示,而這種結構的調整是須要額外的花銷的。因此咱們能夠使用一種帶有 ~
的特殊命令:
XADD mystream MAXLEN ~ 1000 * ... entry fields here ...
複製代碼
它會基於當前的結構合理地對節點執行裁剪,來保證至少會有 1000
條數據,多是 1010
也多是 1030
。
在客戶端消費者讀取 Stream 消息時,Redis 服務器將消息回覆給客戶端的過程當中,客戶端忽然斷開了鏈接,消息就丟失了。可是 PEL 裏已經保存了發出去的消息 ID,待客戶端從新連上以後,能夠再次收到 PEL 中的消息 ID 列表。不過此時 xreadgroup
的起始消息 ID 不能爲參數 >
,而必須是任意有效的消息 ID,通常將參數設爲 0-0
,表示讀取全部的 PEL 消息以及自 last_delivered_id
以後的新消息。
Redis 基於內存存儲,這意味着它會比基於磁盤的 Kafka 快上一些,也意味着使用 Redis 咱們 不能長時間存儲大量數據。不過若是您想以 最小延遲 實時處理消息的話,您能夠考慮 Redis,可是若是 消息很大而且應該重用數據 的話,則應該首先考慮使用 Kafka。
另外從某些角度來講,Redis Stream
也更適用於小型、廉價的應用程序,由於 Kafka
相對來講更難配置一些。
「推薦閱讀 以前的系列文章,裏面 也對 Pub/ Sub 作了詳細的描述:
Redis(8)——發佈/訂閱與Stream - www.wmyskxz.com/2020/03/15/…
Redis 的數據 所有存儲 在 內存 中,若是 忽然宕機,數據就會所有丟失,所以必須有一套機制來保證 Redis 的數據不會由於故障而丟失,這種機制就是 Redis 的 持久化機制,它會將內存中的數據庫狀態 保存到磁盤 中。
咱們來稍微考慮一下 Redis 做爲一個 "內存數據庫" 要作的關於持久化的事情。一般來講,從客戶端發起請求開始,到服務器真實地寫入磁盤,須要發生以下幾件事情:
詳細版 的文字描述大概就是下面這樣:
若是咱們故障僅僅涉及到 軟件層面 (該進程被管理員終止或程序崩潰) 而且沒有接觸到內核,那麼在 上述步驟 3 成功返回以後,咱們就認爲成功了。即便進程崩潰,操做系統仍然會幫助咱們把數據正確地寫入磁盤。
若是咱們考慮 停電/ 火災 等 更具災難性 的事情,那麼只有在完成了第 5 步以後,纔是安全的。
因此咱們能夠總結得出數據安全最重要的階段是:步驟3、4、五,即:
咱們從 第三步 開始。Linux 系統提供了清晰、易用的用於操做文件的 POSIX file API
,20
多年過去,仍然還有不少人對於這一套 API
的設計津津樂道,我想其中一個緣由就是由於你光從 API
的命名就可以很清晰地知道這一套 API 的用途:
int open(const char *path, int oflag, .../*,mode_t mode */);
int close (int filedes);int remove( const char *fname );
ssize_t write(int fildes, const void *buf, size_t nbyte);
ssize_t read(int fildes, void *buf, size_t nbyte);
複製代碼
因此,咱們有很好的可用的 API
來完成 第三步,可是對於成功返回以前,咱們對系統調用花費的時間沒有太多的控制權。
而後咱們來講說 第四步。咱們知道,除了早期對電腦特別瞭解那幫人 (操做系統就這幫人搞的),實際的物理硬件都不是咱們可以 直接操做 的,都是經過 操做系統調用 來達到目的的。爲了防止過慢的 I/O 操做拖慢整個系統的運行,操做系統層面作了不少的努力,譬如說 上述第四步 提到的 寫緩衝區,並非全部的寫操做都會被當即寫入磁盤,而是要先通過一個緩衝區,默認狀況下,Linux 將在 30 秒 後實際提交寫入。
可是很明顯,30 秒 並非 Redis 可以承受的,這意味着,若是發生故障,那麼最近 30 秒內寫入的全部數據均可能會丟失。幸虧 PROSIX API
提供了另外一個解決方案:fsync
,該命令會 強制 內核將 緩衝區 寫入 磁盤,但這是一個很是消耗性能的操做,每次調用都會 阻塞等待 直到設備報告 IO 完成,因此通常在生產環境的服務器中,Redis 一般是每隔 1s 左右執行一次 fsync
操做。
到目前爲止,咱們瞭解到瞭如何控制 第三步
和 第四步
,可是對於 第五步,咱們 徹底沒法控制。也許一些內核實現將試圖告訴驅動實際提交物理介質上的數據,或者控制器可能會爲了提升速度而從新排序寫操做,不會盡快將數據真正寫到磁盤上,而是會等待幾個多毫秒。這徹底是咱們沒法控制的。
「普通人簡單說一下第一條就過了,若是你詳細地對後面兩方面 侃侃而談,那面試官就會對你刮目相看了。
Redis 快照 是最簡單的 Redis 持久性模式。當知足特定條件時,它將生成數據集的時間點快照,例如,若是先前的快照是在 2 分鐘前建立的,而且如今已經至少有 100
次新寫入,則將建立一個新的快照。此條件能夠由用戶配置 Redis 實例來控制,也能夠在運行時修改而無需從新啓動服務器。快照做爲包含整個數據集的單個 .rdb
文件生成。
快照不是很持久。若是運行 Redis 的計算機中止運行,電源線出現故障或者您 kill -9
的實例意外發生,則寫入 Redis 的最新數據將丟失。儘管這對於某些應用程序可能不是什麼大問題,但有些使用案例具備充分的耐用性,在這些狀況下,快照並非可行的選擇。
AOF(Append Only File - 僅追加文件) 它的工做方式很是簡單:每次執行 修改內存 中數據集的寫操做時,都會 記錄 該操做。假設 AOF 日誌記錄了自 Redis 實例建立以來 全部的修改性指令序列,那麼就能夠經過對一個空的 Redis 實例 順序執行全部的指令,也就是 「重放」,來恢復 Redis 當前實例的內存數據結構的狀態。
重啓 Redis 時,咱們不多使用 rdb
來恢復內存狀態,由於會丟失大量數據。咱們一般使用 AOF 日誌重放,可是重放 AOF 日誌性能相對 rdb
來講要慢不少,這樣在 Redis 實例很大的狀況下,啓動須要花費很長的時間。
Redis 4.0 爲了解決這個問題,帶來了一個新的持久化選項——混合持久化。將 rdb
文件的內容和增量的 AOF 日誌文件存在一塊兒。這裏的 AOF 日誌再也不是全量的日誌,而是 自持久化開始到持久化結束 的這段時間發生的增量 AOF 日誌,一般這部分 AOF 日誌很小:
因而在 Redis 重啓的時候,能夠先加載 rdb
的內容,而後再重放增量 AOF 日誌就能夠徹底替代以前的 AOF 全量文件重放,重啓效率所以大幅獲得提高。
「關於兩種持久化方式的更多細節 (原理) 能夠參考:
Redis(7)——持久化【一文了解】 - www.wmyskxz.com/2020/03/13/…
dump.rdb
,
方便持久化。
fork
子進程來完成寫操做,讓主進程繼續處理命令,因此使 IO 最大化。使用單獨子進程來進行持久化,主進程不會進行任何 IO 操做,保證了 Redis 的高性能
appendfsync
屬性,有
always
,每進行一次命令操做就記錄到 aof 文件中一次。
Redis 的數據恢復有着以下的優先級:
拷貝 AOF 文件到 Redis 的數據目錄,啓動 redis-server AOF 的數據恢復過程:Redis 虛擬一個客戶端,讀取 AOF 文件恢復 Redis 命令和參數,而後執行命令從而恢復數據,這些過程主要在 loadAppendOnlyFile()
中實現。
拷貝 RDB 文件到 Redis 的數據目錄,啓動 redis-server 便可,由於 RDB 文件和重啓前保存的是真實數據而不是命令狀態和參數。
主從複製,是指將一臺 Redis 服務器的數據,複製到其餘的 Redis 服務器。前者稱爲 主節點(master),後者稱爲 從節點(slave)。且數據的複製是 單向 的,只能由主節點到從節點。Redis 主從複製支持 主從同步 和 從從同步 兩種,後者是 Redis 後續版本新增的功能,以減輕主節點的同步負擔。
爲了節省篇幅,我把主要的步驟都 濃縮 在了上圖中,其實也能夠 簡化成三個階段:準備階段-數據同步階段-命令傳播階段。
「更多細節 推薦閱讀 以前的系列文章,不只有原理講解,還有實戰環節:
Redis(9)——史上最強【集羣】入門實踐教程 - www.wmyskxz.com/2020/03/17/…
上圖 展現了一個典型的哨兵架構圖,它由兩部分組成,哨兵節點和數據節點:
在複製的基礎上,哨兵實現了 自動化的故障恢復 功能,下方是官方對於哨兵功能的描述:
其中,監控和自動故障轉移功能,使得哨兵能夠及時發現主節點故障並完成轉移。而配置提供者和通知功能,則須要在與客戶端的交互中才能體現。
故障轉移操做的第一步 要作的就是在已下線主服務器屬下的全部從服務器中,挑選出一個狀態良好、數據完整的從服務器,而後向這個從服務器發送 slaveof no one
命令,將這個從服務器轉換爲主服務器。可是這個從服務器是怎麼樣被挑選出來的呢?
簡單來講 Sentinel 使用如下規則來選擇新的主服務器:
「更多細節 推薦閱讀 以前的系列文章,不只有原理講解,還有實戰環節:
Redis(9)——史上最強【集羣】入門實踐教程 - www.wmyskxz.com/2020/03/17/…
上圖 展現了 Redis Cluster 典型的架構圖,集羣中的每個 Redis 節點都 互相兩兩相連,客戶端任意 直連 到集羣中的 任意一臺,就能夠對其餘 Redis 節點進行 讀寫 的操做。
Redis 集羣中內置了 16384
個哈希槽。當客戶端鏈接到 Redis 集羣以後,會同時獲得一份關於這個 集羣的配置信息,當客戶端具體對某一個 key
值進行操做時,會計算出它的一個 Hash 值,而後把結果對 16384
求餘數,這樣每一個 key
都會對應一個編號在 0-16383
之間的哈希槽,Redis 會根據節點數量 大體均等 的將哈希槽映射到不一樣的節點。
再結合集羣的配置信息就可以知道這個 key
值應該存儲在哪個具體的 Redis 節點中,若是不屬於本身管,那麼就會使用一個特殊的 MOVED
命令來進行一個跳轉,告訴客戶端去鏈接這個節點以獲取數據:
GET x
-MOVED 3999 127.0.0.1:6381
複製代碼
MOVED
指令第一個參數 3999
是 key
對應的槽位編號,後面是目標節點地址,MOVED
命令前面有一個減號,表示這是一個錯誤的消息。客戶端在收到 MOVED
指令後,就當即糾正本地的 槽位映射表,那麼下一次再訪問 key
時就可以到正確的地方去獲取了。
bgsave
和
bgrewriteaof
的
fork
操做可能致使主進程阻塞,主從環境下主機切換時可能致使從節點長時間沒法提供服務,全量複製階段主節點的複製緩衝區可能溢出……
Redis 採用方案三。
哈希取餘分區思路很是簡單:計算 key
的 hash 值,而後對節點數量進行取餘,從而決定數據映射到哪一個節點上。
不過該方案最大的問題是,當新增或刪減節點時,節點數量發生變化,系統中全部的數據都須要 從新計算映射關係,引起大規模數據遷移。
一致性哈希算法將 整個哈希值空間 組織成一個虛擬的圓環,範圍是 [0 - 232 - 1],對於每個數據,根據 key
計算 hash 值,確數據在環上的位置,而後今後位置沿順時針行走,找到的第一臺服務器就是其應該映射到的服務器:
與哈希取餘分區相比,一致性哈希分區將 增減節點的影響限制在相鄰節點。以上圖爲例,若是在 node1
和 node2
之間增長 node5
,則只有 node2
中的一部分數據會遷移到 node5
;若是去掉 node2
,則原 node2
中的數據只會遷移到 node4
中,只有 node4
會受影響。
一致性哈希分區的主要問題在於,當 節點數量較少 時,增長或刪減節點,對單個節點的影響可能很大,形成數據的嚴重不平衡。仍是以上圖爲例,若是去掉 node2
,node4
中的數據由總數據的 1/4
左右變爲 1/2
左右,與其餘節點相比負載太高。
該方案在 一致性哈希分區的基礎上,引入了 虛擬節點 的概念。Redis 集羣使用的即是該方案,其中的虛擬節點稱爲 槽(slot)。槽是介於數據和實際節點之間的虛擬概念,每一個實際節點包含必定數量的槽,每一個槽包含哈希值在必定範圍內的數據。
在使用了槽的一致性哈希分區中,槽是數據管理和遷移的基本單位。槽 解耦 了 數據和實際節點 之間的關係,增長或刪除節點對系統的影響很小。仍以上圖爲例,系統中有 4
個實際節點,假設爲其分配 16
個槽(0-15);
若是此時刪除 node2
,只須要將槽 4-7 從新分配便可,例如槽 4-5 分配給 node1
,槽 6 分配給 node3
,槽 7 分配給 node4
;能夠看出刪除 node2
後,數據在其餘節點的分佈仍然較爲均衡。
集羣的創建離不開節點之間的通訊,例如咱們在 快速體驗 中剛啓動六個集羣節點以後經過 redis-cli
命令幫助咱們搭建起來了集羣,實際上背後每一個集羣之間的兩兩鏈接是經過了 CLUSTER MEET <ip> <port>
命令發送 MEET
消息完成的,下面咱們展開詳細說說。
在 哨兵系統 中,節點分爲 數據節點 和 哨兵節點:前者存儲數據,後者實現額外的控制功能。在 集羣 中,沒有數據節點與非數據節點之分:全部的節點都存儲數據,也都參與集羣狀態的維護。爲此,集羣中的每一個節點,都提供了兩個 TCP 端口:
7000
節點的集羣端口爲
17000
。
集羣端口只用於節點之間的通訊,如搭建集羣、增減節點、故障轉移等操做時節點間的通訊;不要使用客戶端鏈接集羣接口。爲了保證集羣能夠正常工做,在配置防火牆時,要同時開啓普通端口和集羣端口。
節點間通訊,按照通訊協議能夠分爲幾種類型:單對單、廣播、Gossip 協議等。重點是廣播和 Gossip 的對比。
集羣中的節點採用 固定頻率(每秒10次) 的 定時任務 進行通訊相關的工做:判斷是否須要發送消息及消息類型、肯定接收節點、發送消息等。若是集羣狀態發生了變化,如增減節點、槽狀態變動,經過節點間的通訊,全部節點會很快得知整個集羣的狀態,使集羣收斂。
節點間發送的消息主要分爲 5
種:meet 消息
、ping 消息
、pong 消息
、fail 消息
、publish 消息
。不一樣的消息類型,通訊協議、發送的頻率和時機、接收節點的選擇等是不一樣的:
CLUSTER MEET
命令時,會向新加入的節點發送
MEET
消息,請求新節點加入到當前集羣;新節點收到 MEET 消息後會回覆一個
PONG
消息。
PING
消息,接收者收到消息後會回覆一個
PONG
消息。
PING 消息的內容是自身節點和部分其餘節點的狀態信息,做用是彼此交換信息,以及檢測節點是否在線。
PING
消息使用 Gossip 協議發送,接收節點的選擇兼顧了收斂速度和帶寬成本,
具體規則以下:(1)隨機找 5 個節點,在其中選擇最久沒有通訊的 1 個節點;(2)掃描節點列表,選擇最近一次收到
PONG
消息時間大於
cluster_node_timeout / 2
的全部節點,防止這些節點長時間未更新。
PONG
消息封裝了自身狀態數據。能夠分爲兩種:
第一種 是在接到
MEET/PING
消息後回覆的
PONG
消息;
第二種 是指節點向集羣廣播
PONG
消息,這樣其餘節點能夠獲知該節點的最新信息,例如故障恢復後新的主節點會廣播
PONG
消息。
FAIL
狀態時,會向集羣廣播這一
FAIL
消息;接收節點會將這一
FAIL
消息保存起來,便於後續的判斷。
PUBLISH
命令後,會先執行該命令,而後向集羣廣播這一消息,接收節點也會執行該
PUBLISH
命令。
節點須要專門的數據結構來存儲集羣的狀態。所謂集羣的狀態,是一個比較大的概念,包括:集羣是否處於上線狀態、集羣中有哪些節點、節點是否可達、節點的主從狀態、槽的分佈……
節點爲了存儲集羣狀態而提供的數據結構中,最關鍵的是 clusterNode
和 clusterState
結構:前者記錄了一個節點的狀態,後者記錄了集羣做爲一個總體的狀態。
clusterNode
結構保存了 一個節點的當前狀態,包括建立時間、節點 id、ip 和端口號等。每一個節點都會用一個 clusterNode
結構記錄本身的狀態,併爲集羣內全部其餘節點都建立一個 clusterNode
結構來記錄節點狀態。
下面列舉了 clusterNode
的部分字段,並說明了字段的含義和做用:
typedef struct clusterNode {
//節點建立時間
mstime_t ctime;
//節點id
char name[REDIS_CLUSTER_NAMELEN];
//節點的ip和端口號
char ip[REDIS_IP_STR_LEN];
int port;
//節點標識:整型,每一個bit都表明了不一樣狀態,如節點的主從狀態、是否在線、是否在握手等
int flags;
//配置紀元:故障轉移時起做用,相似於哨兵的配置紀元
uint64_t configEpoch;
//槽在該節點中的分佈:佔用16384/8個字節,16384個比特;每一個比特對應一個槽:比特值爲1,則該比特對應的槽在節點中;比特值爲0,則該比特對應的槽不在節點中
unsigned char slots[16384/8];
//節點中槽的數量
int numslots;
…………
} clusterNode;
複製代碼
除了上述字段,clusterNode
還包含節點鏈接、主從複製、故障發現和轉移須要的信息等。
clusterState
結構保存了在當前節點視角下,集羣所處的狀態。主要字段包括:
typedef struct clusterState {
//自身節點
clusterNode *myself;
//配置紀元
uint64_t currentEpoch;
//集羣狀態:在線仍是下線
int state;
//集羣中至少包含一個槽的節點數量
int size;
//哈希表,節點名稱->clusterNode節點指針
dict *nodes;
//槽分佈信息:數組的每一個元素都是一個指向clusterNode結構的指針;若是槽尚未分配給任何節點,則爲NULL
clusterNode *slots[16384];
…………
} clusterState;
複製代碼
除此以外,clusterState
還包括故障轉移、槽遷移等須要的信息。
「推薦閱讀 以前的系列文章: Redis(3)——分佈式鎖深刻探究 - www.wmyskxz.com/2020/03/01/…
先拋開 Redis 想一下幾種可能的刪除策略:
在上述的三種策略中定時刪除和按期刪除屬於不一樣時間粒度的 主動刪除,惰性刪除屬於 被動刪除。
Reids 採用的是 惰性刪除和定時刪除 的結合,通常來講能夠藉助最小堆來實現定時器,不過 Redis 的設計考慮到時間事件的有限種類和數量,使用了無序鏈表存儲時間事件,這樣若是在此基礎上實現定時刪除,就意味着 O(N)
遍歷獲取最近須要刪除的數據。
使用 keys
指令能夠掃出指定模式的 key 列表。可是要注意 keys 指令會致使線程阻塞一段時間,線上服務會停頓,直到指令執行完畢,服務才能恢復。這個時候能夠使用 scan
指令,scan
指令能夠無阻塞的提取出指定模式的 key
列表,可是會有必定的重複機率,在客戶端作一次去重就能夠了,可是總體所花費的時間會比直接用 keys
指令長。
「
本文已收錄至個人 Github 程序員成長系列 【More Than Java】,學習,不止 Code,歡迎 star:github.com/wmyskxz/Mor… 我的公衆號 :wmyskxz, 我的獨立域名博客:wmyskxz.com,堅持原創輸出,下方掃碼關注,2020,與您共同成長!
很是感謝各位人才能 看到這裏,若是以爲本篇文章寫得不錯,以爲 「我沒有三顆心臟」有點東西 的話,求點贊,求關注,求分享,求留言!
創做不易,各位的支持和承認,就是我創做的最大動力,咱們下篇文章見!