媽媽再也不擔心我面試被Redis問得臉都綠了
長文前排提醒,收藏向前排提醒,素質三連 (轉發 + 在看 + 留言) 前排提醒!
前言
Redis 作為一個開源的,高階的鍵值儲存和一個適用的解決方案,已經越來越在構建 「高效能」、「可擴充套件」 的 Web 應用上發揮著舉足輕重的作用。
當今網際網路技術架構中 Redis 已然成為了應用得最廣泛的中介軟體之一,它也是中高階後端工程 技術面試 中面試官最喜歡問的工程技能之一,不僅僅要求著我們對 基本的使用 進行掌握,更要深層次地理解 Redis 內部實現 的細節原理。
熟練掌握 Redis,甚至可以毫不誇張地說已經半隻腳踏入心儀的公司了。下面我們一起來盤點回顧一下 Redis 的面試經典問題,就不要再被面試官問得 臉都綠了 呀!
- Ps: 我把 重要的知識點 都做成了 圖片,希望各位 "用餐愉快"。(不錯記得付餐費.. 點個贊留個言..)
一、基礎篇
什麼是 Redis ?
先解釋 Redis 基本概念
Redis (Remote Dictionary Server) 是一個使用 C 語言 編寫的,開源的 (BSD許可) 高效能 非關係型 (NoSQL) 的 鍵值對資料庫。
簡單提一下 Redis 資料結構
Redis 可以儲存 鍵 和 不同型別資料結構值 之間的對映關係。鍵的型別只能是字串,而值除了支援最 基礎的五種資料型別 外,還支援一些 高階資料型別:
一定要說出一些高階資料結構 (當然你自己也要了解.. 下面會說到的別擔心)
,這樣面試官的眼睛才會亮。
Redis 小總結
與傳統資料庫不同的是 Redis 的資料是 存在記憶體 中的,所以 讀寫速度 非常 快,因此 Redis 被廣泛應用於 快取 方向,每秒可以處理超過 10
萬次讀寫操作,是已知效能最快的 Key-Value 資料庫。另外,Redis 也經常用來做 分散式鎖。
除此之外,Redis 支援事務 、持久化、LUA指令碼、LRU驅動事件、多種叢集方案。
Redis 優缺點
優點
- 讀寫效能優異, Redis能讀的速度是
110000
次/s,寫的速度是81000
次/s。 - 支援資料持久化,支援 AOF 和 RDB 兩種持久化方式。
- 支援事務,Redis 的所有操作都是原子性的,同時 Redis 還支援對幾個操作合併後的原子性執行。
- 資料結構豐富,除了支援 string 型別的 value 外還支援 hash、set、zset、list 等資料結構。
- 支援主從複製,主機會自動將資料同步到從機,可以進行讀寫分離。
缺點
- 資料庫 容量受到實體記憶體的限制,不能用作海量資料的高效能讀寫,因此 Redis 適合的場景主要侷限在較小資料量的高效能操作和運算上。
- Redis 不具備自動容錯和恢復功能,主機從機的宕機都會導致前端部分讀寫請求失敗,需要等待機器重啟或者手動切換前端的 IP 才能恢復。
- 主機宕機,宕機前有部分資料未能及時同步到從機,切換 IP 後還會引入資料不一致的問題,降低了 系統的可用性。
- Redis 較難支援線上擴容,在叢集容量達到上限時線上擴容會變得很複雜。為避免這一問題,運維人員在系統上線時必須確保有足夠的空間,這對資源造成了很大的浪費。
為什麼要用快取?為什麼使用 Redis?
提一下現在 Web 應用的現狀
在日常的 Web 應用對資料庫的訪問中,讀操作的次數遠超寫操作,比例大概在 1:9 到 3:7,所以需要讀的可能性是比寫的可能大得多的。當我們使用 SQL 語句去資料庫進行讀寫操作時,資料庫就會 去磁碟把對應的資料索引取回來,這是一個相對較慢的過程。
使用 Redis or 使用快取帶來的優勢
如果我們把資料放在 Redis 中,也就是直接放在記憶體之中,讓服務端直接去讀取記憶體中的資料,那麼這樣 速度 明顯就會快上不少 (高效能),並且會 極大減小資料庫的壓力 (特別是在高併發情況下)。
記得是 兩個角度 啊.. 高效能 和 高併發..
也要提一下使用快取的考慮
但是使用記憶體進行資料儲存開銷也是比較大的,限於成本 的原因,一般我們只是使用 Redis 儲存一些 常用和主要的資料,比如使用者登入的資訊等。
一般而言在使用 Redis 進行儲存的時候,我們需要從以下幾個方面來考慮:
- 業務資料常用嗎?命中率如何? 如果命中率很低,就沒有必要寫入快取;
- 該業務資料是讀操作多,還是寫操作多? 如果寫操作多,頻繁需要寫入資料庫,也沒有必要使用快取;
- 業務資料大小如何? 如果要儲存幾百兆位元組的檔案,會給快取帶來很大的壓力,這樣也沒有必要;
在考慮了這些問題之後,如果覺得有必要使用快取,那麼就使用它!
使用快取會出現什麼問題?
一般來說有如下幾個問題,回答思路遵照 是什麼 → 為什麼 → 怎麼解決:
- 快取雪崩問題;
- 快取穿透問題;
- 快取和資料庫雙寫一致性問題;
快取雪崩問題
另外對於 "Redis 掛掉了,請求全部走資料庫" 這樣的情況,我們還可以有如下的思路:
- 事發前:實現 Redis 的高可用(主從架構 + Sentinel 或者 Redis Cluster),儘量避免 Redis 掛掉這種情況發生。
- 事發中:萬一 Redis 真的掛了,我們可以設定本地快取(ehcache) + 限流(hystrix),儘量避免我們的資料庫被幹掉(起碼能保證我們的服務還是能正常工作的)
- 事發後:Redis 持久化,重啟後自動從磁碟上載入資料,快速恢復快取資料。
快取穿透問題
快取與資料庫雙寫一致問題
雙寫一致性上圖還是稍微粗糙了些,你還需要知道兩種方案 (先操作資料庫和先操作快取) 分別都有什麼優勢和對應的問題,這裡不作贅述,可以參考一下下方的文章,寫得非常詳細。
- 面試前必須要知道的Redis面試題 | Java3y - https://mp.weixin.qq.com/s/3Fmv7h5p2QDtLxc9n1dp5A
Redis 為什麼早期版本選擇單執行緒?
官方解釋
因為 Redis 是基於記憶體的操作,CPU 不是 Redis 的瓶頸,Redis 的瓶頸最有可能是 機器記憶體的大小 或者 網路頻寬。既然單執行緒容易實現,而且 CPU 不會成為瓶頸,那就順理成章地採用單執行緒的方案了。
簡單總結一下
- 使用單執行緒模型能帶來更好的 可維護性,方便開發和除錯;
- 使用單執行緒模型也能 併發 的處理客戶端的請求;(I/O 多路複用機制)
- Redis 服務中執行的絕大多數操作的 效能瓶頸都不是 CPU;
強烈推薦 各位親看一下這篇文章:
- 為什麼 Redis 選擇單執行緒模型 · Why's THE Design? - https://draveness.me/whys-the-design-redis-single-thread
Redis 為什麼這麼快?
簡單總結:
- 純記憶體操作:讀取不需要進行磁碟 I/O,所以比傳統資料庫要快上不少;(但不要有誤區說磁碟就一定慢,例如 Kafka 就是使用磁碟順序讀取但仍然較快)
- 單執行緒,無鎖競爭:這保證了沒有執行緒的上下文切換,不會因為多執行緒的一些操作而降低效能;
- 多路 I/O 複用模型,非阻塞 I/O:採用多路 I/O 複用技術可以讓單個執行緒高效的處理多個網路連線請求(儘量減少網路 IO 的時間消耗);
- 高效的資料結構,加上底層做了大量優化:Redis 對於底層的資料結構和記憶體佔用做了大量的優化,例如不同長度的字串使用不同的結構體表示,HyperLogLog 的密集型儲存結構等等..
二、資料結構篇
簡述一下 Redis 常用資料結構及實現?
首先在 Redis 內部會使用一個 RedisObject 物件來表示所有的 key
和 value
:
其次 Redis 為了 平衡空間和時間效率,針對 value
的具體型別在底層會採用不同的資料結構來實現,下圖展示了他們之間的對映關係:(好像亂糟糟的,但至少能看清楚..)
Redis 的 SDS 和 C 中字串相比有什麼優勢?
先簡單總結一下
C 語言使用了一個長度為 N+1
的字元陣列來表示長度為 N
的字串,並且字元陣列最後一個元素總是 \0
,這種簡單的字串表示方式 不符合 Redis 對字串在安全性、效率以及功能方面的要求。
再來說 C 語言字串的問題
這樣簡單的資料結構可能會造成以下一些問題:
- 獲取字串長度為 O(N) 級別的操作 → 因為 C 不儲存陣列的長度,每次都需要遍歷一遍整個陣列;
- 不能很好的杜絕 緩衝區溢位/記憶體洩漏 的問題 → 跟上述問題原因一樣,如果執行拼接 or 縮短字串的操作,如果操作不當就很容易造成上述問題;
- C 字串 只能儲存文字資料 → 因為 C 語言中的字串必須符合某種編碼(比如 ASCII),例如中間出現的
'\0'
可能會被判定為提前結束的字串而識別不了;
Redis 如何解決的 | SDS 的優勢
如果去看 Redis 的原始碼 sds.h/sdshdr
檔案,你會看到 SDS 完整的實現細節,這裡簡單來說一下 Redis 如何解決的:
- 多增加 len 表示當前字串的長度:這樣就可以直接獲取長度了,複雜度 O(1);
- 自動擴充套件空間:當 SDS 需要對字串進行修改時,首先借助於
len
和alloc
檢查空間是否滿足修改所需的要求,如果空間不夠的話,SDS 會自動擴充套件空間,避免了像 C 字串操作中的覆蓋情況; - 有效降低記憶體分配次數:C 字串在涉及增加或者清除操作時會改變底層陣列的大小造成重新分配,SDS 使用了 空間預分配 和 惰性空間釋放 機制,簡單理解就是每次在擴充套件時是成倍的多分配的,在縮容是也是先留著並不正式歸還給 OS;
- 二進位制安全:C 語言字串只能儲存
ascii
碼,對於圖片、音訊等資訊無法儲存,SDS 是二進位制安全的,寫入什麼讀取就是什麼,不做任何過濾和限制;
字典是如何實現的?Rehash 瞭解嗎?
先總體聊一下 Redis 中的字典
字典是 Redis 伺服器中出現最為頻繁的複合型資料結構。除了 hash 結構的資料會用到字典外,整個 Redis 資料庫的所有 key
和 value
也組成了一個 全域性字典,還有帶過期時間的 key
也是一個字典。(儲存在 RedisDb 資料結構中)
說明字典內部結構和 rehash
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)——跳躍表 - https://www.wmyskxz.com/2020/02/29/redis-2-tiao-yue-biao/
HyperLogLog 有了解嗎?
建議閱讀 之前的系列文章:
- Redis(4)——神奇的HyperLoglog解決統計問題 - https://www.wmyskxz.com/2020/03/02/reids-4-shen-qi-de-hyperloglog-jie-jue-tong-ji-wen-ti/
布隆過濾器有了解嗎?
建議閱讀 之前的系列文章:
- Redis(5)——億級資料過濾和布隆過濾器 - https://www.wmyskxz.com/2020/03/11/redis-5-yi-ji-shu-ju-guo-lu-he-bu-long-guo-lu-qi/
GeoHash 瞭解嗎?
建議閱讀 之前的系列文章:
- Redis(6)——GeoHash查詢附近的人 - https://www.wmyskxz.com/2020/03/12/redis-6-geohash-cha-zhao-fu-jin-de-ren/
壓縮列表瞭解嗎?
這是 Redis 為了節約記憶體 而使用的一種資料結構,zset 和 hash 容器物件會在元素個數較少的時候,採用壓縮列表(ziplist)進行儲存。壓縮列表是 一塊連續的記憶體空間,元素之間緊挨著儲存,沒有任何冗餘空隙。
因為之前自己也沒有學習過,所以找了一篇比較好比較容易理解的文章:
- 圖解Redis之資料結構篇——壓縮列表 - https://mp.weixin.qq.com/s/nba0FUEAVRs0vi24KUoyQg
- 這一篇稍微底層稍微硬核一點:http://www.web-lovers.com/redis-source-ziplist.html
快速列表 quicklist 瞭解嗎?
Redis 早期版本儲存 list 列表資料結構使用的是壓縮列表 ziplist 和普通的雙向連結串列 linkedlist,也就是說當元素少時使用 ziplist,當元素多時用 linkedlist。但考慮到連結串列的附加空間相對較高,prev
和 next
指標就要佔去 16
個位元組(64 位作業系統佔用 8
個位元組),另外每個節點的記憶體都是單獨分配,會傢俱記憶體的碎片化,影響記憶體管理效率。
後來 Redis 新版本(3.2)對列表資料結構進行了改造,使用 quicklist
代替了 ziplist
和 linkedlist
。
同上..建議閱讀一下以下的文章:
- Redis列表list 底層原理 - https://zhuanlan.zhihu.com/p/102422311
Stream 結構有了解嗎?
Redis Stream 從概念上來說,就像是一個 僅追加內容 的 訊息連結串列,把所有加入的訊息都一個一個串起來,每個訊息都有一個唯一的 ID 和內容,這很簡單,讓它複雜的是從 Kafka 借鑑的另一種概念:消費者組(Consumer Group) (思路一致,實現不同):
上圖就展示了一個典型的 Stream 結構。每個 Stream 都有唯一的名稱,它就是 Redis 的 key
,在我們首次使用 xadd
指令追加訊息時自動建立。我們對圖中的一些概念做一下解釋:
- Consumer Group:消費者組,可以簡單看成記錄流狀態的一種資料結構。消費者既可以選擇使用
XREAD
命令進行 獨立消費,也可以多個消費者同時加入一個消費者組進行 組內消費。同一個消費者組內的消費者共享所有的 Stream 資訊,同一條訊息只會有一個消費者消費到,這樣就可以應用在分散式的應用場景中來保證訊息的唯一性。 - last_delivered_id:用來表示消費者組消費在 Stream 上 消費位置 的遊標資訊。每個消費者組都有一個 Stream 內 唯一的名稱,消費者組不會自動建立,需要使用
XGROUP CREATE
指令來顯式建立,並且需要指定從哪一個訊息 ID 開始消費,用來初始化last_delivered_id
這個變數。 - pending_ids:每個消費者內部都有的一個狀態變數,用來表示 已經 被客戶端 獲取,但是 還沒有 ack 的訊息。記錄的目的是為了 保證客戶端至少消費了訊息一次,而不會在網路傳輸的中途丟失而沒有對訊息進行處理。如果客戶端沒有 ack,那麼這個變數裡面的訊息 ID 就會越來越多,一旦某個訊息被 ack,它就會對應開始減少。這個變數也被 Redis 官方稱為 PEL (Pending Entries List)。
Stream 訊息太多怎麼辦?
很容易想到,要是訊息積累太多,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
。
PEL 是如何避免訊息丟失的?
在客戶端消費者讀取 Stream 訊息時,Redis 伺服器將訊息回覆給客戶端的過程中,客戶端突然斷開了連線,訊息就丟失了。但是 PEL 裡已經儲存了發出去的訊息 ID,待客戶端重新連上之後,可以再次收到 PEL 中的訊息 ID 列表。不過此時 xreadgroup
的起始訊息 ID 不能為引數 >
,而必須是任意有效的訊息 ID,一般將引數設為 0-0
,表示讀取所有的 PEL 訊息以及自 last_delivered_id
之後的新訊息。
和 Kafka 對比起來呢?
Redis 基於記憶體儲存,這意味著它會比基於磁碟的 Kafka 快上一些,也意味著使用 Redis 我們 不能長時間儲存大量資料。不過如果您想以 最小延遲 實時處理訊息的話,您可以考慮 Redis,但是如果 訊息很大並且應該重用資料 的話,則應該首先考慮使用 Kafka。
另外從某些角度來說,Redis Stream
也更適用於小型、廉價的應用程式,因為 Kafka
相對來說更難配置一些。
推薦閱讀 之前的系列文章,裡面 也對 Pub/ Sub 做了詳細的描述:
- Redis(8)——釋出/訂閱與Stream - https://www.wmyskxz.com/2020/03/15/redis-8-fa-bu-ding-yue-yu-stream/
三、持久化篇
什麼是持久化?
先簡單談一談是什麼
Redis 的資料 全部儲存 在 記憶體 中,如果 突然宕機,資料就會全部丟失,因此必須有一套機制來保證 Redis 的資料不會因為故障而丟失,這種機制就是 Redis 的 持久化機制,它會將記憶體中的資料庫狀態 儲存到磁碟 中。
解釋一下持久化發生了什麼
我們來稍微考慮一下 Redis 作為一個 "記憶體資料庫" 要做的關於持久化的事情。通常來說,從客戶端發起請求開始,到伺服器真實地寫入磁碟,需要發生如下幾件事情:
詳細版 的文字描述大概就是下面這樣:
- 客戶端向資料庫 傳送寫命令 (資料在客戶端的記憶體中)
- 資料庫 接收 到客戶端的 寫請求 (資料在伺服器的記憶體中)
- 資料庫 呼叫系統 API 將資料寫入磁碟 (資料在核心緩衝區中)
- 作業系統將 寫緩衝區 傳輸到 磁碟控控制器 (資料在磁碟快取中)
- 作業系統的磁碟控制器將資料 寫入實際的物理媒介 中 (資料在磁碟中)
分析如何保證持久化安全
如果我們故障僅僅涉及到 軟體層面 (該程序被管理員終止或程式崩潰) 並且沒有接觸到核心,那麼在 上述步驟 3 成功返回之後,我們就認為成功了。即使程序崩潰,作業系統仍然會幫助我們把資料正確地寫入磁碟。
如果我們考慮 停電/ 火災 等 更具災難性 的事情,那麼只有在完成了第 5 步之後,才是安全的。
所以我們可以總結得出資料安全最重要的階段是:步驟三、四、五,即:
- 資料庫軟體呼叫寫操作將使用者空間的緩衝區轉移到核心緩衝區的頻率是多少?
- 核心多久從緩衝區取資料重新整理到磁碟控制器?
- 磁碟控制器多久把資料寫入物理媒介一次?
- 注意: 如果真的發生災難性的事件,我們可以從上圖的過程中看到,任何一步都可能被意外打斷丟失,所以只能 儘可能地保證 資料的安全,這對於所有資料庫來說都是一樣的。
我們從 第三步 開始。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 設計最佳實踐的思考 - https://www.cnblogs.com/yuanjiangw/p/10846560.html
所以,我們有很好的可用的 API
來完成 第三步,但是對於成功返回之前,我們對系統呼叫花費的時間沒有太多的控制權。
然後我們來說說 第四步。我們知道,除了早期對電腦特別瞭解那幫人 (作業系統就這幫人搞的),實際的物理硬體都不是我們能夠 直接操作 的,都是通過 作業系統呼叫 來達到目的的。為了防止過慢的 I/O 操作拖慢整個系統的執行,作業系統層面做了很多的努力,譬如說 上述第四步 提到的 寫緩衝區,並不是所有的寫操作都會被立即寫入磁碟,而是要先經過一個緩衝區,預設情況下,Linux 將在 30 秒 後實際提交寫入。
但是很明顯,30 秒 並不是 Redis 能夠承受的,這意味著,如果發生故障,那麼最近 30 秒內寫入的所有資料都可能會丟失。幸好 PROSIX API
提供了另一個解決方案:fsync
,該命令會 強制 核心將 緩衝區 寫入 磁碟,但這是一個非常消耗效能的操作,每次呼叫都會 阻塞等待 直到裝置報告 IO 完成,所以一般在生產環境的伺服器中,Redis 通常是每隔 1s 左右執行一次 fsync
操作。
到目前為止,我們瞭解到瞭如何控制 第三步
和 第四步
,但是對於 第五步,我們 完全無法控制。也許一些核心實現將試圖告訴驅動實際提交物理介質上的資料,或者控制器可能會為了提高速度而重新排序寫操作,不會盡快將資料真正寫到磁碟上,而是會等待幾個多毫秒。這完全是我們無法控制的。
普通人簡單說一下第一條就過了,如果你詳細地對後面兩方面 侃侃而談,那面試官就會對你另眼相看了。
Redis 中的兩種持久化方式?
方式一:快照
Redis 快照 是最簡單的 Redis 永續性模式。當滿足特定條件時,它將生成資料集的時間點快照,例如,如果先前的快照是在 2 分鐘前建立的,並且現在已經至少有 100
次新寫入,則將建立一個新的快照。此條件可以由使用者配置 Redis 例項來控制,也可以在執行時修改而無需重新啟動伺服器。快照作為包含整個資料集的單個 .rdb
檔案生成。
方式二:AOF
快照不是很持久。如果執行 Redis 的計算機停止執行,電源線出現故障或者您 kill -9
的例項意外發生,則寫入 Redis 的最新資料將丟失。儘管這對於某些應用程式可能不是什麼大問題,但有些使用案例具有充分的耐用性,在這些情況下,快照並不是可行的選擇。
AOF(Append Only File - 僅追加檔案) 它的工作方式非常簡單:每次執行 修改記憶體 中資料集的寫操作時,都會 記錄 該操作。假設 AOF 日誌記錄了自 Redis 例項建立以來 所有的修改性指令序列,那麼就可以通過對一個空的 Redis 例項 順序執行所有的指令,也就是 「重放」,來恢復 Redis 當前例項的記憶體資料結構的狀態。
Redis 4.0 的混合持久化
重啟 Redis 時,我們很少使用 rdb
來恢復記憶體狀態,因為會丟失大量資料。我們通常使用 AOF 日誌重放,但是重放 AOF 日誌效能相對 rdb
來說要慢很多,這樣在 Redis 例項很大的情況下,啟動需要花費很長的時間。
Redis 4.0 為了解決這個問題,帶來了一個新的持久化選項——混合持久化。將 rdb
檔案的內容和增量的 AOF 日誌檔案存在一起。這裡的 AOF 日誌不再是全量的日誌,而是 自持久化開始到持久化結束 的這段時間發生的增量 AOF 日誌,通常這部分 AOF 日誌很小:
於是在 Redis 重啟的時候,可以先載入 rdb
的內容,然後再重放增量 AOF 日誌就可以完全替代之前的 AOF 全量檔案重放,重啟效率因此大幅得到提升。
關於兩種持久化方式的更多細節 (原理) 可以參考:
- Redis(7)——持久化【一文了解】 - https://www.wmyskxz.com/2020/03/13/redis-7-chi-jiu-hua-yi-wen-liao-jie/
RDB 和 AOF 各自有什麼優缺點?
RDB | 優點
- 只有一個檔案
dump.rdb
,方便持久化。 - 容災性好,一個檔案可以儲存到安全的磁碟。
- 效能最大化,
fork
子程序來完成寫操作,讓主程序繼續處理命令,所以使 IO 最大化。使用單獨子程序來進行持久化,主程序不會進行任何 IO 操作,保證了 Redis 的高效能 - 相對於資料集大時,比 AOF 的 啟動效率 更高。
RDB | 缺點
- 資料安全性低。RDB 是間隔一段時間進行持久化,如果持久化之間 Redis 發生故障,會發生資料丟失。所以這種方式更適合資料要求不嚴謹的時候;
AOF | 優點
- 資料安全,aof 持久化可以配置
appendfsync
屬性,有always
,每進行一次命令操作就記錄到 aof 檔案中一次。 - 通過 append 模式寫檔案,即使中途伺服器宕機,可以通過 redis-check-aof 工具解決資料一致性問題。
- AOF 機制的 rewrite 模式。AOF 檔案沒被 rewrite 之前(檔案過大時會對命令 進行合併重寫),可以刪除其中的某些命令(比如誤操作的 flushall)
AOF | 缺點
- AOF 檔案比 RDB 檔案大,且 恢復速度慢。
- 資料集大 的時候,比 rdb 啟動效率低。
兩種方式如何選擇?
- 一般來說, 如果想達到足以媲美 PostgreSQL 的 資料安全性,你應該 同時使用兩種持久化功能。在這種情況下,當 Redis 重啟的時候會優先載入 AOF 檔案來恢復原始的資料,因為在通常情況下 AOF 檔案儲存的資料集要比 RDB 檔案儲存的資料集要完整。
- 如果你非常關心你的資料, 但仍然 可以承受數分鐘以內的資料丟失,那麼你可以 只使用 RDB 持久化。
- 有很多使用者都只使用 AOF 持久化,但並不推薦這種方式,因為定時生成 RDB 快照(snapshot)非常便於進行資料庫備份, 並且 RDB 恢復資料集的速度也要比 AOF 恢復的速度要快,除此之外,使用 RDB 還可以避免 AOF 程式的 bug。
- 如果你只希望你的資料在伺服器執行的時候存在,你也可以不使用任何持久化方式。
Redis 的資料恢復
Redis 的資料恢復有著如下的優先順序:
- 如果只配置 AOF ,重啟時載入 AOF 檔案恢復資料;
- 如果同時配置了 RDB 和 AOF ,啟動只加載 AOF 檔案恢復資料;
- 如果只配置 RDB,啟動將載入 dump 檔案恢復資料。
拷貝 AOF 檔案到 Redis 的資料目錄,啟動 redis-server AOF 的資料恢復過程:Redis 虛擬一個客戶端,讀取 AOF 檔案恢復 Redis 命令和引數,然後執行命令從而恢復資料,這些過程主要在 loadAppendOnlyFile()
中實現。
拷貝 RDB 檔案到 Redis 的資料目錄,啟動 redis-server 即可,因為 RDB 檔案和重啟前儲存的是真實資料而不是命令狀態和引數。
四、叢集篇
主從同步瞭解嗎?
主從複製,是指將一臺 Redis 伺服器的資料,複製到其他的 Redis 伺服器。前者稱為 主節點(master),後者稱為 從節點(slave)。且資料的複製是 單向 的,只能由主節點到從節點。Redis 主從複製支援 主從同步 和 從從同步 兩種,後者是 Redis 後續版本新增的功能,以減輕主節點的同步負擔。
主從複製主要的作用
- 資料冗餘: 主從複製實現了資料的熱備份,是持久化之外的一種資料冗餘方式。
- 故障恢復: 當主節點出現問題時,可以由從節點提供服務,實現快速的故障恢復 (實際上是一種服務的冗餘)。
- 負載均衡: 在主從複製的基礎上,配合讀寫分離,可以由主節點提供寫服務,由從節點提供讀服務 (即寫 Redis 資料時應用連線主節點,讀 Redis 資料時應用連線從節點),分擔伺服器負載。尤其是在寫少讀多的場景下,通過多個從節點分擔讀負載,可以大大提高 Redis 伺服器的併發量。
- 高可用基石: 除了上述作用以外,主從複製還是哨兵和叢集能夠實施的 基礎,因此說主從複製是 Redis 高可用的基礎。
實現原理
為了節省篇幅,我把主要的步驟都 濃縮 在了上圖中,其實也可以 簡化成三個階段:準備階段-資料同步階段-命令傳播階段。
更多細節 推薦閱讀 之前的系列文章,不僅有原理講解,還有實戰環節:
- Redis(9)——史上最強【叢集】入門實踐教程 - https://www.wmyskxz.com/2020/03/17/redis-9-shi-shang-zui-qiang-ji-qun-ru-men-shi-jian-jiao-cheng/
哨兵模式瞭解嗎?
上圖 展示了一個典型的哨兵架構圖,它由兩部分組成,哨兵節點和資料節點:
- 哨兵節點: 哨兵系統由一個或多個哨兵節點組成,哨兵節點是特殊的 Redis 節點,不儲存資料;
- 資料節點: 主節點和從節點都是資料節點;
在複製的基礎上,哨兵實現了 自動化的故障恢復 功能,下方是官方對於哨兵功能的描述:
- 監控(Monitoring): 哨兵會不斷地檢查主節點和從節點是否運作正常。
- 自動故障轉移(Automatic failover): 當 主節點 不能正常工作時,哨兵會開始 自動故障轉移操作,它會將失效主節點的其中一個 從節點升級為新的主節點,並讓其他從節點改為複製新的主節點。
- 配置提供者(Configuration provider): 客戶端在初始化時,通過連線哨兵來獲得當前 Redis 服務的主節點地址。
- 通知(Notification): 哨兵可以將故障轉移的結果傳送給客戶端。
其中,監控和自動故障轉移功能,使得哨兵可以及時發現主節點故障並完成轉移。而配置提供者和通知功能,則需要在與客戶端的互動中才能體現。
新的主伺服器是怎樣被挑選出來的?
故障轉移操作的第一步 要做的就是在已下線主伺服器屬下的所有從伺服器中,挑選出一個狀態良好、資料完整的從伺服器,然後向這個從伺服器傳送 slaveof no one
命令,將這個從伺服器轉換為主伺服器。但是這個從伺服器是怎麼樣被挑選出來的呢?
簡單來說 Sentinel 使用以下規則來選擇新的主伺服器:
- 在失效主伺服器屬下的從伺服器當中, 那些被標記為主觀下線、已斷線、或者最後一次回覆 PING 命令的時間大於五秒鐘的從伺服器都會被 淘汰。
- 在失效主伺服器屬下的從伺服器當中, 那些與失效主伺服器連線斷開的時長超過 down-after 選項指定的時長十倍的從伺服器都會被 淘汰。
- 在 經歷了以上兩輪淘汰之後 剩下來的從伺服器中, 我們選出 複製偏移量(replication offset)最大 的那個 從伺服器 作為新的主伺服器;如果複製偏移量不可用,或者從伺服器的複製偏移量相同,那麼 帶有最小執行 ID 的那個從伺服器成為新的主伺服器。
更多細節 推薦閱讀 之前的系列文章,不僅有原理講解,還有實戰環節:
- Redis(9)——史上最強【叢集】入門實踐教程 - https://www.wmyskxz.com/2020/03/17/redis-9-shi-shang-zui-qiang-ji-qun-ru-men-shi-jian-jiao-cheng/
Redis 叢集使用過嗎?原理?
上圖 展示了 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
時就能夠到正確的地方去獲取了。
叢集的主要作用
- 資料分割槽: 資料分割槽 (或稱資料分片) 是叢集最核心的功能。叢集將資料分散到多個節點,一方面 突破了 Redis 單機記憶體大小的限制,儲存容量大大增加;另一方面 每個主節點都可以對外提供讀服務和寫服務,大大提高了叢集的響應能力。Redis 單機記憶體大小受限問題,在介紹持久化和主從複製時都有提及,例如,如果單機記憶體太大,
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);
- 槽 0-3 位於 node1;4-7 位於 node2;以此類推....
如果此時刪除 node2
,只需要將槽 4-7 重新分配即可,例如槽 4-5 分配給 node1
,槽 6 分配給 node3
,槽 7 分配給 node4
;可以看出刪除 node2
後,資料在其他節點的分佈仍然較為均衡。
節點之間的通訊機制瞭解嗎?
叢集的建立離不開節點之間的通訊,例如我們在 快速體驗 中剛啟動六個叢集節點之後通過 redis-cli
命令幫助我們搭建起來了叢集,實際上背後每個叢集之間的兩兩連線是通過了 CLUSTER MEET <ip> <port>
命令傳送 MEET
訊息完成的,下面我們展開詳細說說。
兩個埠
在 哨兵系統 中,節點分為 資料節點 和 哨兵節點:前者儲存資料,後者實現額外的控制功能。在 叢集 中,沒有資料節點與非資料節點之分:所有的節點都儲存資料,也都參與叢集狀態的維護。為此,叢集中的每個節點,都提供了兩個 TCP 埠:
- 普通埠: 即我們在前面指定的埠 (7000等)。普通埠主要用於為客戶端提供服務 (與單機節點類似);但在節點間資料遷移時也會使用。
- 叢集埠: 埠號是普通埠 + 10000 (10000是固定值,無法改變),如
7000
節點的叢集埠為17000
。叢集埠只用於節點之間的通訊,如搭建叢集、增減節點、故障轉移等操作時節點間的通訊;不要使用客戶端連線叢集介面。為了保證叢集可以正常工作,在配置防火牆時,要同時開啟普通埠和叢集埠。
Gossip 協議
節點間通訊,按照通訊協議可以分為幾種型別:單對單、廣播、Gossip 協議等。重點是廣播和 Gossip 的對比。
- 廣播是指向叢集內所有節點發送訊息。優點 是叢集的收斂速度快(叢集收斂是指叢集內所有節點獲得的叢集資訊是一致的),缺點 是每條訊息都要傳送給所有節點,CPU、頻寬等消耗較大。
- Gossip 協議的特點是:在節點數量有限的網路中,每個節點都 “隨機” 的與部分節點通訊 (並不是真正的隨機,而是根據特定的規則選擇通訊的節點),經過一番雜亂無章的通訊,每個節點的狀態很快會達到一致。Gossip 協議的 優點 有負載 (比廣播) 低、去中心化、容錯性高 (因為通訊有冗餘) 等;缺點 主要是叢集的收斂速度慢。
訊息型別
叢集中的節點採用 固定頻率(每秒10次) 的 定時任務 進行通訊相關的工作:判斷是否需要傳送訊息及訊息型別、確定接收節點、傳送訊息等。如果叢集狀態發生了變化,如增減節點、槽狀態變更,通過節點間的通訊,所有節點會很快得知整個叢集的狀態,使叢集收斂。
節點間傳送的訊息主要分為 5
種:meet 訊息
、ping 訊息
、pong 訊息
、fail 訊息
、publish 訊息
。不同的訊息型別,通訊協議、傳送的頻率和時機、接收節點的選擇等是不同的:
- MEET 訊息: 在節點握手階段,當節點收到客戶端的
CLUSTER MEET
命令時,會向新加入的節點發送MEET
訊息,請求新節點加入到當前叢集;新節點收到 MEET 訊息後會回覆一個PONG
訊息。 - PING 訊息: 叢集裡每個節點每秒鐘會選擇部分節點發送
PING
訊息,接收者收到訊息後會回覆一個PONG
訊息。PING 訊息的內容是自身節點和部分其他節點的狀態資訊,作用是彼此交換資訊,以及檢測節點是否線上。PING
訊息使用 Gossip 協議傳送,接收節點的選擇兼顧了收斂速度和頻寬成本,具體規則如下:(1)隨機找 5 個節點,在其中選擇最久沒有通訊的 1 個節點;(2)掃描節點列表,選擇最近一次收到PONG
訊息時間大於cluster_node_timeout / 2
的所有節點,防止這些節點長時間未更新。 - PONG訊息:
PONG
訊息封裝了自身狀態資料。可以分為兩種:第一種 是在接到MEET/PING
訊息後回覆的PONG
訊息;第二種 是指節點向叢集廣播PONG
訊息,這樣其他節點可以獲知該節點的最新資訊,例如故障恢復後新的主節點會廣播PONG
訊息。 - FAIL 訊息: 當一個主節點判斷另一個主節點進入
FAIL
狀態時,會向叢集廣播這一FAIL
訊息;接收節點會將這一FAIL
訊息儲存起來,便於後續的判斷。 - PUBLISH 訊息: 節點收到
PUBLISH
命令後,會先執行該命令,然後向叢集廣播這一訊息,接收節點也會執行該PUBLISH
命令。
叢集資料如何儲存的有了解嗎?
節點需要專門的資料結構來儲存叢集的狀態。所謂叢集的狀態,是一個比較大的概念,包括:叢集是否處於上線狀態、叢集中有哪些節點、節點是否可達、節點的主從狀態、槽的分佈……
節點為了儲存叢集狀態而提供的資料結構中,最關鍵的是 clusterNode
和 clusterState
結構:前者記錄了一個節點的狀態,後者記錄了叢集作為一個整體的狀態。
clusterNode 結構
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 結構
clusterState
結構儲存了在當前節點視角下,叢集所處的狀態。主要欄位包括:
typedef struct clusterState {
//自身節點
clusterNode *myself;
//配置紀元
uint64_t currentEpoch;
//叢集狀態:線上還是下線
int state;
//叢集中至少包含一個槽的節點數量
int size;
//雜湊表,節點名稱->clusterNode節點指標
dict *nodes;
//槽分佈資訊:陣列的每個元素都是一個指向clusterNode結構的指標;如果槽還沒有分配給任何節點,則為NULL
clusterNode *slots[16384];
…………
} clusterState;
除此之外,clusterState
還包括故障轉移、槽遷移等需要的資訊。
五、其他問題
Redis 如何實現分散式鎖?
推薦閱讀 之前的系列文章:
Redis(3)——分散式鎖深入探究 - https://www.wmyskxz.com/2020/03/01/redis-3/
Redis 過期鍵的刪除策略?
簡單描述
先拋開 Redis 想一下幾種可能的刪除策略:
- 定時刪除:在設定鍵的過期時間的同時,建立一個定時器 timer). 讓定時器在鍵的過期時間來臨時,立即執行對鍵的刪除操作。
- 惰性刪除:放任鍵過期不管,但是每次從鍵空間中獲取鍵時,都檢查取得的鍵是否過期,如果過期的話,就刪除該鍵;如果沒有過期,就返回該鍵。
- 定期刪除:每隔一段時間程式就對資料庫進行一次檢查,刪除裡面的過期鍵。至於要刪除多少過期鍵,以及要檢查多少個數據庫,則由演算法決定。
在上述的三種策略中定時刪除和定期刪除屬於不同時間粒度的 主動刪除,惰性刪除屬於 被動刪除。
三種策略都有各自的優缺點
- 定時刪除對記憶體使用率有優勢,但是對 CPU 不友好;
- 惰性刪除對記憶體不友好,如果某些鍵值對一直不被使用,那麼會造成一定量的記憶體浪費;
- 定期刪除是定時刪除和惰性刪除的折中。
Redis 中的實現
Reids 採用的是 惰性刪除和定時刪除 的結合,一般來說可以藉助最小堆來實現定時器,不過 Redis 的設計考慮到時間事件的有限種類和數量,使用了無序連結串列儲存時間事件,這樣如果在此基礎上實現定時刪除,就意味著 O(N)
遍歷獲取最近需要刪除的資料。
Redis 的淘汰策略有哪些?
Redis 有六種淘汰策略
策略 | 描述 |
---|---|
volatile-lru | 從已設定過期時間的 KV 集中優先對最近最少使用(less recently used)的資料淘汰 |
volitile-ttl | 從已設定過期時間的 KV 集中優先對剩餘時間短(time to live)的資料淘汰 |
volitile-random | 從已設定過期時間的 KV 集中隨機選擇資料淘汰 |
allkeys-lru | 從所有 KV 集中優先對最近最少使用(less recently used)的資料淘汰 |
allKeys-random | 從所有 KV 集中隨機選擇資料淘汰 |
noeviction | 不淘汰策略,若超過最大記憶體,返回錯誤資訊 |
4.0 版本後增加以下兩種
- volatile-lfu:從已設定過期時間的資料集(server.db[i].expires)中挑選最不經常使用的資料淘汰
- allkeys-lfu:當記憶體不足以容納新寫入資料時,在鍵空間中,移除最不經常使用的 key
Redis常見效能問題和解決方案?
- Master 最好不要做任何持久化工作,包括記憶體快照和 AOF 日誌檔案,特別是不要啟用記憶體快照做持久化。
- 如果資料比較關鍵,某個 Slave 開啟 AOF 備份資料,策略為每秒同步一次。
- 為了主從複製的速度和連線的穩定性,Slave 和 Master 最好在同一個區域網內。
- 儘量避免在壓力較大的主庫上增加從庫。
- Master 呼叫 BGREWRITEAOF 重寫 AOF 檔案,AOF 在重寫的時候會佔大量的 CPU 和記憶體資源,導致服務 load 過高,出現短暫服務暫停現象。
- 為了 Master 的穩定性,主從複製不要用圖狀結構,用單向連結串列結構更穩定,即主從關係為:Master<–Slave1<–Slave2<–Slave3…,這樣的結構也方便解決單點故障問題,實現 Slave 對 Master 的替換,也即,如果 Master 掛了,可以立馬啟用 Slave1 做 Master,其他不變。
假如Redis裡面有1億個key,其中有10w個key是以某個固定的已知的字首開頭的,如何將它們全部找出來?
使用 keys
指令可以掃出指定模式的 key 列表。但是要注意 keys 指令會導致執行緒阻塞一段時間,線上服務會停頓,直到指令執行完畢,服務才能恢復。這個時候可以使用 scan
指令,scan
指令可以無阻塞的提取出指定模式的 key
列表,但是會有一定的重複概率,在客戶端做一次去重就可以了,但是整體所花費的時間會比直接用 keys
指令長。
More..
參考資料
- 3w字深度好文|Redis面試全攻略,讀完這個就可以和麵試官大戰幾個回合了 - https://mp.weixin.qq.com/s/f9N13fnyTtnu2D5sKZiu9w
- 大廠面試!我和麵試官之間關於Redis的一場對弈! - [https://mp.weixin.qq.com/s/DHTPSfmWTZpdTmlytzLz1g](https://mp.weixin.qq.com/s/DHTPSfmWTZpdTmlytzLz1g
- Redis面試題(2020最新版) - https://blog.csdn.net/ThinkWon/article/details/103522351
- 本文已收錄至我的 Github 程式設計師成長系列 【More Than Java】,學習,不止 Code,歡迎 star:https://github.com/wmyskxz/MoreThanJava
- 個人公眾號 :wmyskxz,個人獨立域名部落格:wmyskxz.com,堅持原創輸出,下方掃碼關注,2020,與您共同成長!
非常感謝各位人才能 看到這裡,如果覺得本篇文章寫得不錯,覺得 「我沒有三顆心臟」有點東西 的話,求點贊,求關注,求分享,求留言!
創作不易,各位的支援和認可,就是我創作的最大動力,我們下篇文章見!