Redis常见面试题详解
文章目錄
- 1. Redis
- 1.1 Redis可以用來做什么?
- 1.2 Redis和傳統的關系型數據庫有什么不同?
- 1.3 Redis有哪些數據類型?
- 1.4 Redis是單線程的,為什么還能這么快?
- 1.5 Redis在持久化時fork出一個子進程,這時已經有兩個進程了,怎么能說是單線程呢?
- 1.6 set和zset有什么區別?
- 1.7 說一下Redis中的watch命令
- 1.8 說說Redis中List結構的相關操作
- 1.9 你要如何設計Redis的過期時間?
- 1.10 Redis中,sexnx命令的返回值是什么,如何使用該命令實現分布式鎖?
- 1.11 說一說Redis的持久化策略
- 1.12 如何實現Redis的高可用?
- 1.13 Redis的主從同步是如何實現的?
- 1.14 Redis為什么存的快,內存斷電數據怎么恢復?
- 1.15 說一說Redis的緩存淘汰策略
- 1.16 請介紹一下Redis的過期策略
- 1.17 緩存穿透、緩存擊穿、緩存雪崩有什么區別,該如何解決?
- 1.18 如何保證緩存與數據庫的雙寫一致性?
- 1.19 請介紹Redis集群的實現方案
- 1.20 說一說Redis集群的分片機制
- 1.21 說一說Redis集群的應用和優劣勢
- 1.22 說一說hash類型底層的數據結構
- 1.23 介紹一下zset類型底層的數據結構
- 1.24 如何利用Redis實現分布式Session?
- 1.25 如何利用Redis實現一個分布式鎖?
- 1.26 說一說你對布隆過濾器的理解
- 1.27 多臺Redis抗高并發訪問該怎么設計?
- 1.28 如果并發量超過30萬,怎么設計Redis架構?
- 1.27 多臺Redis抗高并發訪問該怎么設計?
- 1.28 如果并發量超過30萬,怎么設計Redis架構?
1. Redis
1.1 Redis可以用來做什么?
參考答案
1.2 Redis和傳統的關系型數據庫有什么不同?
參考答案
Redis是一種基于鍵值對的NoSQL數據庫,而鍵值對的值是由多種數據結構和算法組成的。Redis的數據都存儲于內存中,因此它的速度驚人,讀寫性能可達10萬/秒,遠超關系型數據庫。
關系型數據庫是基于二維數據表來存儲數據的,它的數據格式更為嚴謹,并支持關系查詢。關系型數據庫的數據存儲于磁盤上,可以存放海量的數據,但性能遠不如Redis。
1.3 Redis有哪些數據類型?
參考答案
1.4 Redis是單線程的,為什么還能這么快?
參考答案
關于Redis的單線程架構實現,如下圖:
1.5 Redis在持久化時fork出一個子進程,這時已經有兩個進程了,怎么能說是單線程呢?
參考答案
Redis是單線程的,主要是指Redis的網絡IO和鍵值對讀寫是由一個線程來完成的。而Redis的其他功能,如持久化、異步刪除、集群數據同步等,則是依賴其他線程來執行的。所以,說Redis是單線程的只是一種習慣的說法,事實上它的底層不是單線程的。
1.6 set和zset有什么區別?
參考答案
set:
- 集合中的元素是無序、不可重復的,一個集合最多能存儲232-1個元素;
- 集合除了支持對元素的增刪改查之外,還支持對多個集合取交集、并集、差集。
zset:
- 有序集合保留了集合元素不能重復的特點;
- 有序集合會給每個元素設置一個分數,并以此作為排序的依據;
- 有序集合不能包含相同的元素,但是不同元素的分數可以相同。
1.7 說一下Redis中的watch命令
參考答案
很多時候,要確保事務中的數據沒有被其他客戶端修改才執行該事務。Redis提供了watch命令來解決這類問題,這是一種樂觀鎖的機制。客戶端通過watch命令,要求服務器對一個或多個key進行監視,如果在客戶端執行事務之前,這些key發生了變化,則服務器將拒絕執行客戶端提交的事務,并向它返回一個空值。
1.8 說說Redis中List結構的相關操作
參考答案
列表是線性有序的數據結構,它內部的元素是可以重復的,并且一個列表最多能存儲2^32-1個元素。列表包含如下的常用命令:
- lpush/rpush:從列表的左側/右側添加數據;
- lrange:指定索引范圍,并返回這個范圍內的數據;
- lindex:返回指定索引處的數據;
- lpop/rpop:從列表的左側/右側彈出一個數據;
- blpop/brpop:從列表的左側/右側彈出一個數據,若列表為空則進入阻塞狀態。
1.9 你要如何設計Redis的過期時間?
參考答案
1.10 Redis中,sexnx命令的返回值是什么,如何使用該命令實現分布式鎖?
參考答案
setnx命令返回整數值,當返回1時表示設置值成果,當返回0時表示設置值失敗(key已存在)。
一般我們不建議直接使用setnx命令來實現分布式鎖,因為為了避免出現死鎖,我們要給鎖設置一個自動過期時間。而setnx命令和設置過期時間的命令不是原子的,可能加鎖成果而設置過期時間失敗,依然存在死鎖的隱患。對于這種情況,Redis改進了set命令,給它增加了nx選項,啟用該選項時set命令的效果就會setnx一樣了。
采用Redis實現分布式鎖,就是在Redis里存一份代表鎖的數據,通常用字符串即可。采用改進后的setnx命令(即set...nx...命令)實現分布式鎖的思路,以及優化的過程如下:
加鎖:
第一版,這種方式的缺點是容易產生死鎖,因為客戶端有可能忘記解鎖,或者解鎖失敗。
setnx key value第二版,給鎖增加了過期時間,避免出現死鎖。但這兩個命令不是原子的,第二步可能會失敗,依然無法避免死鎖問題。
setnx key value``expire key seconds第三版,通過“set…nx…”命令,將加鎖、過期命令編排到一起,它們是原子操作了,可以避免死鎖。
set key value nx ex seconds解鎖:
解鎖就是刪除代表鎖的那份數據。
del key問題:
看起來已經很完美了,但實際上還有隱患,如下圖。進程A在任務沒有執行完畢時,鎖已經到期被釋放了。等進程A的任務執行結束后,它依然會嘗試釋放鎖,因為它的代碼邏輯就是任務結束后釋放鎖。但是,它的鎖早已自動釋放過了,它此時釋放的可能是其他線程的鎖。
想要解決這個問題,我們需要解決兩件事情:
按照以上思路,優化后的命令如下:
# 加鎖 set key random-value nx ex seconds # 解鎖 if redis.call("get",KEYS[1]) == ARGV[1] thenreturn redis.call("del",KEYS[1]) elsereturn 0 end1.11 說一說Redis的持久化策略
參考答案
Redis支持RDB持久化、AOF持久化、RDB-AOF混合持久化這三種持久化方式。
RDB:
RDB(Redis Database)是Redis默認采用的持久化方式,它以快照的形式將進程數據持久化到硬盤中。RDB會創建一個經過壓縮的二進制文件,文件以“.rdb”結尾,內部存儲了各個數據庫的鍵值對數據等信息。RDB持久化的觸發方式有兩種:
- 手動觸發:通過SAVE或BGSAVE命令觸發RDB持久化操作,創建“.rdb”文件;
- 自動觸發:通過配置選項,讓服務器在滿足指定條件時自動執行BGSAVE命令。
其中,SAVE命令執行期間,Redis服務器將阻塞,直到“.rdb”文件創建完畢為止。而BGSAVE命令是異步版本的SAVE命令,它會使用Redis服務器進程的子進程,創建“.rdb”文件。BGSAVE命令在創建子進程時會存在短暫的阻塞,之后服務器便可以繼續處理其他客戶端的請求。總之,BGSAVE命令是針對SAVE阻塞問題做的優化,Redis內部所有涉及RDB的操作都采用BGSAVE的方式,而SAVE命令已經廢棄!
BGSAVE命令的執行流程,如下圖:
BGSAVE命令的原理,如下圖:
RDB持久化的優缺點如下:
-
優點:RDB生成緊湊壓縮的二進制文件,體積小,使用該文件恢復數據的速度非常快;
-
缺點:BGSAVE每次運行都要執行fork操作創建子進程,屬于重量級操作,不宜頻繁執行,
所以RDB持久化沒辦法做到實時的持久化。
AOF:
AOF(Append Only File),解決了數據持久化的實時性,是目前Redis持久化的主流方式。AOF以獨立日志的方式,記錄了每次寫入命令,重啟時再重新執行AOF文件中的命令來恢復數據。AOF的工作流程包括:命令寫入(append)、文件同步(sync)、文件重寫(rewrite)、重啟加載(load),如下圖:
AOF默認不開啟,需要修改配置項來啟用它:
appendonly yes # 啟用AOF appendfilename "appendonly.aof" # 設置文件名AOF以文本協議格式寫入命令,如:
*3\r\n$3\r\nset\r\n$5\r\nhello\r\n$5\r\nworld\r\n文本協議格式具有如下的優點:
AOF持久化的文件同步機制:
為了提高程序的寫入性能,現代操作系統會把針對硬盤的多次寫操作優化為一次寫操作。
這種優化機制雖然提高了性能,但也給程序的寫入操作帶來了不確定性。
appendfsync選項的取值和含義如下:
AOF持久化的優缺點如下:
- 優點:與RDB持久化可能丟失大量的數據相比,AOF持久化的安全性要高很多。通過使用everysec選項,用戶可以將數據丟失的時間窗口限制在1秒之內。
- 缺點:AOF文件存儲的是協議文本,它的體積要比二進制格式的”.rdb”文件大很多。AOF需要通過執行AOF文件中的命令來恢復數據庫,其恢復速度比RDB慢很多。AOF在進行重寫時也需要創建子進程,在數據庫體積較大時將占用大量資源,會導致服務器的短暫阻塞。
RDB-AOF混合持久化:
Redis從4.0開始引入RDB-AOF混合持久化模式,這種模式是基于AOF持久化構建而來的。用戶可以通過配置文件中的“aof-use-rdb-preamble yes”配置項開啟AOF混合持久化。Redis服務器在執行AOF重寫操作時,會按照如下原則處理數據:
- 像執行BGSAVE命令一樣,根據數據庫當前的狀態生成相應的RDB數據,并將其寫入AOF文件中;
- 對于重寫之后執行的Redis命令,則以協議文本的方式追加到AOF文件的末尾,即RDB數據之后。
通過使用RDB-AOF混合持久化,用戶可以同時獲得RDB持久化和AOF持久化的優點,服務器既可以通過AOF文件包含的RDB數據來實現快速的數據恢復操作,又可以通過AOF文件包含的AOF數據來將丟失數據的時間窗口限制在1s之內。
1.12 如何實現Redis的高可用?
參考答案
實現Redis的高可用,主要有哨兵和集群兩種方式。
哨兵:
Redis Sentinel(哨兵)是一個分布式架構,它包含若干個哨兵節點和數據節點。每個哨兵節點會對數據節點和其余的哨兵節點進行監控,當發現節點不可達時,會對節點做下線標識。如果被標識的是主節點,它就會與其他的哨兵節點進行協商,當多數哨兵節點都認為主節點不可達時,它們便會選舉出一個哨兵節點來完成自動故障轉移的工作,同時還會將這個變化實時地通知給應用方。整個過程是自動的,不需要人工介入,有效地解決了Redis的高可用問題!
一組哨兵可以監控一個主節點,也可以同時監控多個主節點,兩種情況的拓撲結構如下圖:
哨兵節點包含如下的特征:
集群:
Redis集群采用虛擬槽分區來實現數據分片,它把所有的鍵根據哈希函數映射到0-16383整數槽內,計算公式為slot=CRC16(key)&16383,每一個節點負責維護一部分槽以及槽所映射的鍵值數據。虛擬槽分區具有如下特點:
Redis集群中數據的分片邏輯如下圖:
1.13 Redis的主從同步是如何實現的?
參考答案
從2.8版本開始,Redis使用psync命令完成主從數據同步,同步過程分為全量復制和部分復制。全量復制一般用于初次復制的場景,部分復制則用于處理因網絡中斷等原因造成數據丟失的場景。psync命令需要以下參數的支持:
psync命令的執行過程以及返回結果,如下圖:
全量復制的過程,如下圖:
部分復制的過程,如下圖:
1.14 Redis為什么存的快,內存斷電數據怎么恢復?
參考答案
Redis存的快是因為它的數據都存放在內存里,并且為了保證數據的安全性,Redis還提供了三種數據的持久化機制,即RDB持久化、AOF持久化、RDB-AOF混合持久化。若服務器斷電,那么我們可以利用持久化文件,對數據進行恢復。理論上來說,AOF/RDB-AOF持久化可以將丟失數據的窗口控制在1S之內。
1.15 說一說Redis的緩存淘汰策略
參考答案
當寫入數據將導致超出maxmemory限制時,Redis會采用maxmemory-policy所指定的策略進行數據淘汰,該策略一共包含如下8種選項:
| noeviction | 直接返回錯誤; | |
| volatile-ttl | 從設置了過期時間的鍵中,選擇過期時間最小的鍵,進行淘汰; | |
| volatile-random | 從設置了過期時間的鍵中,隨機選擇鍵,進行淘汰; | |
| volatile-lru | 從設置了過期時間的鍵中,使用LRU算法選擇鍵,進行淘汰; | |
| volatile-lfu | 從設置了過期時間的鍵中,使用LFU算法選擇鍵,進行淘汰; | 4.0 |
| allleys-random | 從所有的鍵中,隨機選擇鍵,進行淘汰; | |
| allkeys-lru | 從所有的鍵中,使用LRU算法選擇鍵,進行淘汰; | |
| allkeys-lfu | 從所有的鍵中,使用LFU算法選擇鍵,進行淘汰; | 4.0 |
其中,volatile前綴代表從設置了過期時間的鍵中淘汰數據,allkeys前綴代表從所有的鍵中淘汰數據。關于后綴,ttl代表選擇過期時間最小的鍵,random代表隨機選擇鍵,需要我們額外關注的是lru和lfu后綴,它們分別代表采用lru算法和lfu算法來淘汰數據。
LRU(Least Recently Used)是按照最近最少使用原則來篩選數據,即最不常用的數據會被篩選出來!
- 標準LRU:把所有的數據組成一個鏈表,表頭和表尾分別表示MRU和LRU端,即最常使用端和最少使用端。剛被訪問的數據會被移動到MRU端,而新增的數據也是剛被訪問的數據,也會被移動到MRU端。當鏈表的空間被占滿時,它會刪除LRU端的數據。
- 近似LRU:Redis會記錄每個數據的最近一次訪問的時間戳(LRU)。Redis執行寫入操作時,若發現內存超出maxmemory,就會執行一次近似LRU淘汰算法。近似LRU會隨機采樣N個key,然后淘汰掉最舊的key,若淘汰后內存依然超出限制,則繼續采樣淘汰。可以通過maxmemory_samples配置項,設置近似LRU每次采樣的數據個數,該配置項的默認值為5。
LRU算法的不足之處在于,若一個key很少被訪問,只是剛剛偶爾被訪問了一次,則它就被認為是熱點數據,短時間內不會被淘汰。
LFU算法正式用于解決上述問題,LFU(Least Frequently Used)是Redis4新增的淘汰策略,它根據key的最近訪問頻率進行淘汰。LFU在LRU的基礎上,為每個數據增加了一個計數器,來統計這個數據的訪問次數。當使用LFU策略淘汰數據時,首先會根據數據的訪問次數進行篩選,把訪問次數最低的數據淘汰出內存。如果兩個數據的訪問次數相同,LFU再比較這兩個數據的訪問時間,把訪問時間更早的數據淘汰出內存。
1.16 請介紹一下Redis的過期策略
參考答案
Redis支持如下兩種過期策略:
惰性刪除:客戶端訪問一個key的時候,Redis會先檢查它的過期時間,如果發現過期就立刻刪除這個key。
定期刪除:Redis會將設置了過期時間的key放到一個獨立的字典中,并對該字典進行每秒10次的過期掃描,
過期掃描不會遍歷字典中所有的key,而是采用了一種簡單的貪心策略。該策略的刪除邏輯如下:
1.17 緩存穿透、緩存擊穿、緩存雪崩有什么區別,該如何解決?
參考答案
緩存穿透:
問題描述:
客戶端查詢根本不存在的數據,使得請求直達存儲層,導致其負載過大,甚至宕機。出現這種情況的原因,可能是業務層誤將緩存和庫中的數據刪除了,也可能是有人惡意攻擊,專門訪問庫中不存在的數據。
解決方案:
緩存擊穿:
問題描述:
一份熱點數據,它的訪問量非常大。在其緩存失效的瞬間,大量請求直達存儲層,導致服務崩潰。
解決方案:
緩存雪崩:
問題描述:
在某一時刻,緩存層無法繼續提供服務,導致所有的請求直達存儲層,造成數據庫宕機。可能是緩存中有大量數據同時過期,也可能是Redis節點發生故障,導致大量請求無法得到處理。
解決方案:
1.18 如何保證緩存與數據庫的雙寫一致性?
參考答案
四種同步策略:
想要保證緩存與數據庫的雙寫一致,一共有4種方式,即4種同步策略:
從這4種同步策略中,我們需要作出比較的是:
更新緩存還是刪除緩存:
下面,我們來分析一下,應該采用更新緩存還是刪除緩存的方式。
-
更新緩存
優點:每次數據變化都及時更新緩存,所以查詢時不容易出現未命中的情況。
缺點:更新緩存的消耗比較大。如果數據需要經過復雜的計算再寫入緩存,那么頻繁的更新緩存,就會影響服務器的性能。如果是寫入數據頻繁的業務場景,那么可能頻繁的更新緩存時,卻沒有業務讀取該數據。
-
刪除緩存
優點:操作簡單,無論更新操作是否復雜,都是將緩存中的數據直接刪除。
缺點:刪除緩存后,下一次查詢緩存會出現未命中,這時需要重新讀取一次數據庫。
從上面的比較來看,一般情況下,刪除緩存是更優的方案。
先操作數據庫還是緩存:
下面,我們再來分析一下,應該先操作數據庫還是先操作緩存。
首先,我們將先刪除緩存與先更新數據庫,在出現失敗時進行一個對比:
如上圖,是先刪除緩存再更新數據庫,在出現失敗時可能出現的問題:
最終,緩存和數據庫的數據是一致的,但仍然是舊的數據。而我們的期望是二者數據一致,并且是新的數據。
如上圖,是先更新數據庫再刪除緩存,在出現失敗時可能出現的問題:
最終,緩存和數據庫的數據是不一致的。
經過上面的比較,我們發現在出現失敗的時候,是無法明確分辨出先刪緩存和先更新數據庫哪個方式更好,以為它們都存在問題。后面我們會進一步對這兩種方式進行比較,但是在這里我們先探討一下,上述場景出現的問題,應該如何解決呢?
實際上,無論上面我們采用哪種方式去同步緩存與數據庫,在第二步出現失敗的時候,都建議采用重試機制解決,因為最終我們是要解決掉這個錯誤的。而為了避免重試機制影響主要業務的執行,一般建議重試機制采用異步的方式執行,如下圖:
這里我們按照先更新數據庫,再刪除緩存的方式,來說明重試機制的主要步驟:
好了,下面我們再將先刪緩存與先更新數據庫,在沒有出現失敗時進行對比:
如上圖,是先刪除緩存再更新數據庫,在沒有出現失敗時可能出現的問題:
可見,進程A的兩步操作均成功,但由于存在并發,在這兩步之間,進程B訪問了緩存。最終結果是,緩存中存儲了舊的數據,而數據庫中存儲了新的數據,二者數據不一致。
如上圖,是先更新數據庫再刪除緩存,再沒有出現失敗時可能出現的問題:
可見,最終緩存與數據庫的數據是一致的,并且都是最新的數據。但進程B在這個過程里讀到了舊的數據,可能還有其他進程也像進程B一樣,在這兩步之間讀到了緩存中舊的數據,但因為這兩步的執行速度會比較快,所以影響不大。對于這兩步之后,其他進程再讀取緩存數據的時候,就不會出現類似于進程B的問題了。
最終結論:
經過對比你會發現,先更新數據庫、再刪除緩存是影響更小的方案。如果第二步出現失敗的情況,則可以采用重試機制解決問題。
擴展閱讀
延時雙刪
上面我們提到,如果是先刪緩存、再更新數據庫,在沒有出現失敗時可能會導致數據的不一致。如果在實際的應用中,出于某些考慮我們需要選擇這種方式,那有辦法解決這個問題嗎?答案是有的,那就是采用延時雙刪的策略,延時雙刪的基本思路如下:
阻塞一段時間之后,再次刪除緩存,就可以把這個過程中緩存中不一致的數據刪除掉。而具體的時間,要評估你這項業務的大致時間,按照這個時間來設定即可。
采用讀寫分離的架構怎么辦?
如果數據庫采用的是讀寫分離的架構,那么又會出現新的問題,如下圖:
進程A先刪除緩存,再更新主數據庫,然后主庫將數據同步到從庫。而在主從數據庫同步之前,可能會有進程B訪問了緩存,發現數據不存在,進而它去訪問從庫獲取到舊的數據,然后同步到緩存。這樣,最終也會導致緩存與數據庫的數據不一致。這個問題的解決方案,依然是采用延時雙刪的策略,但是在評估延長時間的時候,要考慮到主從數據庫同步的時間。
第二次刪除失敗了怎么辦?
如果第二次刪除依然失敗,則可以增加重試的次數,但是這個次數要有限制,當超出一定的次數時,要采取報錯、記日志、發郵件提醒等措施。
1.19 請介紹Redis集群的實現方案
參考答案
Redis集群的分區方案:
Redis集群采用虛擬槽分區來實現數據分片,它把所有的鍵根據哈希函數映射到0-16383整數槽內,計算公式為slot=CRC16(key)&16383,每一個節點負責維護一部分槽以及槽所映射的鍵值數據。虛擬槽分區具有如下特點:
Redis集群中數據的分片邏輯如下圖:
Redis集群的功能限制:
Redis集群方案在擴展了Redis處理能力的同時,也帶來了一些使用上的限制:
Redis集群的通信方案:
在分布式存儲中需要提供維護節點元數據信息的機制,所謂元數據是指:節點負責哪些數據,是否出現故障等狀態信息。常見的元數據維護方式分為:集中式和P2P方式。
Redis集群采用P2P的Gossip(流言)協議,Gossip協議的工作原理就是節點彼此不斷通信交換信息,一段時間后所有的節點都會知道集群完整的信息,這種方式類似流言傳播。通信的大致過程如下:
其中,Gossip協議的主要職責就是信息交換,而信息交換的載體就是節點彼此發送的Gossip消息,Gossip消息分為:meet消息、ping消息、pong消息、fail消息等。
- meet消息:用于通知新節點加入,消息發送者通知接受者加入到當前集群。meet消息通信正常完成后,接收節點會加入到集群中并進行周期性的ping、pong消息交換。
- ping消息:集群內交換最頻繁的消息,集群內每個節點每秒向多個其他節點發送ping消息,用于檢測節點是否在線和交換彼此狀態信息。ping消息封裝了自身節點和一部分其他節點的狀態數據。
- pong消息:當接收到meet、ping消息時,作為響應消息回復給發送方確認消息正常通信。pong消息內封裝了自身狀態數據,節點也可以向集群內廣播自身的pong消息來通知整個集群對自身狀態進行更新。
- fail消息:當節點判定集群內另一個節點下線時,會向集群內廣播一個fail消息,其他節點接收到fail消息之后把對應節點更新為下線狀態。
雖然Gossip協議的信息交換機制具有天然的分布式特性,但它是有成本的。因為Redis集群內部需要頻繁地進行節點信息交換,而ping/pong消息會攜帶當前節點和部分其他節點的狀態數據,勢必會加重帶寬和計算的負擔。所以,Redis集群的Gossip協議需要兼顧信息交換的實時性和成本的開銷。
- 集群里的每個節點默認每隔一秒鐘就會從已知節點列表中隨機選出五個節點,然后對這五個節點中最長時間沒有發送過PING消息的節點發送PING消息,以此來檢測被選中的節點是否在線。
- 如果節點A最后一次收到節點B發送的PONG消息的時間,距離當前時間已經超過了節點A的超時選項設置時長的一半(cluster-node-timeout/2),那么節點A也會向節點B發送PING消息,這可以防止節點A因為長時間沒有隨機選中節點B作為PING消息的發送對象而導致對節點B的信息更新滯后。
- 每個消息主要的數據占用:slots槽數組(2KB)和整個集群1/10的狀態數據(10個節點狀態數據約1KB)。
1.20 說一說Redis集群的分片機制
參考答案
Redis集群采用虛擬槽分區來實現數據分片,它把所有的鍵根據哈希函數映射到0-16383整數槽內,計算公式為slot=CRC16(key)&16383,每一個節點負責維護一部分槽以及槽所映射的鍵值數據。虛擬槽分區具有如下特點:
Redis集群中數據的分片邏輯如下圖:
1.21 說一說Redis集群的應用和優劣勢
參考答案
優勢:
Redis Cluster是Redis的分布式解決方案,在3.0版本正式推出,有效地解決了Redis分布式方面的需求。當遇到單機內存、并發、流量等瓶頸時,可以采用Cluster架構方案達到負載均衡的目的。
劣勢:
Redis集群方案在擴展了Redis處理能力的同時,也帶來了一些使用上的限制:
1.22 說一說hash類型底層的數據結構
參考答案
哈希對象有兩種編碼方案,當同時滿足以下條件時,哈希對象采用ziplist編碼,否則采用hashtable編碼:
- 哈希對象保存的鍵值對數量小于512個;
- 哈希對象保存的所有鍵值對中的鍵和值,其字符串長度都小于64字節。
其中,ziplist編碼采用壓縮列表作為底層實現,而hashtable編碼采用字典作為底層實現。
壓縮列表:
壓縮列表(ziplist),是Redis為了節約內存而設計的一種線性數據結構,它是由一系列具有特殊編碼的連續內存塊構成的。一個壓縮列表可以包含任意多個節點,每個節點可以保存一個字節數組或一個整數值。
壓縮列表的結構如下圖所示:
該結構當中的字段含義如下表所示:
| zlbytes | uint32_t | 4字節 | 壓縮列表占用的內存字節數; |
| zltail | uint32_t | 4字節 | 壓縮列表表尾節點距離列表起始地址的偏移量(單位字節); |
| zllen | uint16_t | 2字節 | 壓縮列表包含的節點數量,等于UINT16_MAX時,需遍歷列表計算真實數量; |
| entryX | 列表節點 | 不固定 | 壓縮列表包含的節點,節點的長度由節點所保存的內容決定; |
| zlend | uint8_t | 1字節 | 壓縮列表的結尾標識,是一個固定值0xFF; |
其中,壓縮列表的節點由以下字段構成:
previous_entry_length(pel)屬性以字節為單位,記錄當前節點的前一節點的長度,其自身占據1字節或5字節:
基于“pel”屬性,程序便可以通過指針運算,根據當前節點的起始地址計算出前一節點的起始地址,從而實現從表尾向表頭的遍歷操作。
content屬性負責保存節點的值(字節數組或整數),其類型和長度則由encoding屬性決定,它們的關系如下:
| 00 xxxxxx | 1字節 | 最大長度為26 -1的字節數組; |
| 01 xxxxxx bbbbbbbb | 2字節 | 最大長度為214-1的字節數組; |
| 10 __ bbbbbbbb … … … | 5字節 | 最大長度為232-1的字節數組; |
| 11 000000 | 1字節 | int16_t類型的整數; |
| 11 010000 | 1字節 | int32_t類型的整數; |
| 11 100000 | 1字節 | int64_t類型的整數; |
| 11 110000 | 1字節 | 24位有符號整數; |
| 11 111110 | 1字節 | 8位有符號整數; |
| 11 11xxxx | 1字節 | 沒有content屬性,xxxx直接存[0,12]范圍的整數值; |
字典:
字典(dict)又稱為散列表,是一種用來存儲鍵值對的數據結構。C語言沒有內置這種數據結構,所以Redis構建了自己的字典實現。
Redis字典的實現主要涉及三個結構體:字典、哈希表、哈希表節點。其中,每個哈希表節點保存一個鍵值對,每個哈希表由多個哈希表節點構成,而字典則是對哈希表的進一步封裝。這三個結構體的關系如下圖所示:
其中,dict代表字典,dictht代表哈希表,dictEntry代表哈希表節點。可以看出,dictEntry是一個數組,這很好理解,因為一個哈希表里要包含多個哈希表節點。而dict里包含2個dictht,多出的哈希表用于REHASH。當哈希表保存的鍵值對數量過多或過少時,需要對哈希表的大小進行擴展或收縮操作,在Redis中,擴展和收縮哈希表是通過REHASH實現的,執行REHASH的大致步驟如下:
為字典的ht[1]哈希表分配內存空間
如果執行的是擴展操作,則ht[1]的大小為第1個大于等于ht[0].used*2的2n。如果執行的是收縮操作,則ht[1]的大小為第1個大于等于ht[0].used的2n。
將存儲在ht[0]中的數據遷移到ht[1]上
重新計算鍵的哈希值和索引值,然后將鍵值對放置到ht[1]哈希表的指定位置上。
將字典的ht[1]哈希表晉升為默認哈希表
遷移完成后,清空ht[0],再交換ht[0]和ht[1]的值,為下一次REHASH做準備。
當滿足以下任何一個條件時,程序會自動開始對哈希表執行擴展操作:
為了避免REHASH對服務器性能造成影響,REHASH操作不是一次性地完成的,而是分多次、漸進式地完成的。漸進式REHASH的詳細過程如下:
REHSH期間,字典同時持有兩個哈希表,此時的訪問將按照如下原則處理:
1.23 介紹一下zset類型底層的數據結構
參考答案
有序集合對象有2種編碼方案,當同時滿足以下條件時,集合對象采用ziplist編碼,否則采用skiplist編碼:
- 有序集合保存的元素數量不超過128個;
- 有序集合保存的所有元素的成員長度都小于64字節。
其中,ziplist編碼的有序集合采用壓縮列表作為底層實現,skiplist編碼的有序集合采用zset結構作為底層實現。
其中,zset是一個復合結構,它的內部采用字典和跳躍表來實現,其源碼如下。其中,dict保存了從成員到分支的映射關系,zsl則按分值由小到大保存了所有的集合元素。這樣,當按照成員來訪問有序集合時可以直接從dict中取值,當按照分值的范圍訪問有序集合時可以直接從zsl中取值,采用了空間換時間的策略以提高訪問效率。
typedef struct zset {dict *dict; // 字典,保存了從成員到分值的映射關系;zskiplist *zsl; // 跳躍表,按分值由小到大保存所有集合元素; } zset;綜上,zset對象的底層數據結構包括:壓縮列表、字典、跳躍表。
壓縮列表:
壓縮列表(ziplist),是Redis為了節約內存而設計的一種線性數據結構,它是由一系列具有特殊編碼的連續內存塊構成的。一個壓縮列表可以包含任意多個節點,每個節點可以保存一個字節數組或一個整數值。
壓縮列表的結構如下圖所示:
該結構當中的字段含義如下表所示:
| zlbytes | uint32_t | 4字節 | 壓縮列表占用的內存字節數; |
| zltail | uint32_t | 4字節 | 壓縮列表表尾節點距離列表起始地址的偏移量(單位字節); |
| zllen | uint16_t | 2字節 | 壓縮列表包含的節點數量,等于UINT16_MAX時,需遍歷列表計算真實數量; |
| entryX | 列表節點 | 不固定 | 壓縮列表包含的節點,節點的長度由節點所保存的內容決定; |
| zlend | uint8_t | 1字節 | 壓縮列表的結尾標識,是一個固定值0xFF; |
其中,壓縮列表的節點由以下字段構成:
previous_entry_length(pel)屬性以字節為單位,記錄當前節點的前一節點的長度,其自身占據1字節或5字節:
基于“pel”屬性,程序便可以通過指針運算,根據當前節點的起始地址計算出前一節點的起始地址,從而實現從表尾向表頭的遍歷操作。
content屬性負責保存節點的值(字節數組或整數),其類型和長度則由encoding屬性決定,它們的關系如下:
| 00 xxxxxx | 1字節 | 最大長度為26 -1的字節數組; |
| 01 xxxxxx bbbbbbbb | 2字節 | 最大長度為214-1的字節數組; |
| 10 __ bbbbbbbb … … … | 5字節 | 最大長度為232-1的字節數組; |
| 11 000000 | 1字節 | int16_t類型的整數; |
| 11 010000 | 1字節 | int32_t類型的整數; |
| 11 100000 | 1字節 | int64_t類型的整數; |
| 11 110000 | 1字節 | 24位有符號整數; |
| 11 111110 | 1字節 | 8位有符號整數; |
| 11 11xxxx | 1字節 | 沒有content屬性,xxxx直接存[0,12]范圍的整數值; |
字典:
字典(dict)又稱為散列表,是一種用來存儲鍵值對的數據結構。C語言沒有內置這種數據結構,所以Redis構建了自己的字典實現。
Redis字典的實現主要涉及三個結構體:字典、哈希表、哈希表節點。其中,每個哈希表節點保存一個鍵值對,每個哈希表由多個哈希表節點構成,而字典則是對哈希表的進一步封裝。這三個結構體的關系如下圖所示:
其中,dict代表字典,dictht代表哈希表,dictEntry代表哈希表節點。可以看出,dictEntry是一個數組,這很好理解,因為一個哈希表里要包含多個哈希表節點。而dict里包含2個dictht,多出的哈希表用于REHASH。當哈希表保存的鍵值對數量過多或過少時,需要對哈希表的大小進行擴展或收縮操作,在Redis中,擴展和收縮哈希表是通過REHASH實現的,執行REHASH的大致步驟如下:
為字典的ht[1]哈希表分配內存空間
如果執行的是擴展操作,則ht[1]的大小為第1個大于等于ht[0].used*2的2n。如果執行的是收縮操作,則ht[1]的大小為第1個大于等于ht[0].used的2n。
將存儲在ht[0]中的數據遷移到ht[1]上
重新計算鍵的哈希值和索引值,然后將鍵值對放置到ht[1]哈希表的指定位置上。
將字典的ht[1]哈希表晉升為默認哈希表
遷移完成后,清空ht[0],再交換ht[0]和ht[1]的值,為下一次REHASH做準備。
當滿足以下任何一個條件時,程序會自動開始對哈希表執行擴展操作:
為了避免REHASH對服務器性能造成影響,REHASH操作不是一次性地完成的,而是分多次、漸進式地完成的。漸進式REHASH的詳細過程如下:
REHSH期間,字典同時持有兩個哈希表,此時的訪問將按照如下原則處理:
跳躍表:
跳躍表的查找復雜度為平均O(logN),最壞O(N),效率堪比紅黑樹,卻遠比紅黑樹實現簡單。跳躍表是在鏈表的基礎上,通過增加索引來提高查找效率的。
有序鏈表插入、刪除的復雜度為O(1),而查找的復雜度為O(N)。例:若要查找值為60的元素,需要從第1個元素依次向后比較,共需比較6次才行,如下圖:
跳躍表是從有序鏈表中選取部分節點,組成一個新鏈表,并以此作為原始鏈表的一級索引。再從一級索引中選取部分節點,組成一個新鏈表,并以此作為原始鏈表的二級索引。以此類推,可以有多級索引,如下圖:
跳躍表在查找時,優先從高層開始查找,若next節點值大于目標值,或next指針指向NULL,則從當前節點下降一層繼續向后查找,這樣便可以提高查找的效率了。
跳躍表的實現主要涉及2個結構體:zskiplist、zskiplistNode,它們的關系如下圖所示:
其中,藍色的表格代表zskiplist,紅色的表格代表zskiplistNode。zskiplist有指向頭尾節點的指針,以及列表的長度,列表中最高的層級。zskiplistNode的頭節點是空的,它不存儲任何真實的數據,它擁有最高的層級,但這個層級不記錄在zskiplist之內。
1.24 如何利用Redis實現分布式Session?
參考答案
在web開發中,我們會把用戶的登錄信息存儲在session里。而session是依賴于cookie的,即服務器創建session時會給它分配一個唯一的ID,并且在響應時創建一個cookie用于存儲這個SESSIONID。當客戶端收到這個cookie之后,就會自動保存這個SESSIONID,并且在下次訪問時自動攜帶這個SESSIONID,屆時服務器就可以通過這個SESSIONID得到與之對應的session,從而識別用戶的身。如下圖:
現在的互聯網應用,基本都是采用分布式部署方式,即將應用程序部署在多臺服務器上,并通過nginx做統一的請求分發。而服務器與服務器之間是隔離的,它們的session是不共享的,這就存在session同步的問題了,如下圖:
如果客戶端第一次訪問服務器,請求被分發到了服務器A上,則服務器A會為該客戶端創建session。如果客戶端再次訪問服務器,請求被分發到服務器B上,則由于服務器B中沒有這個session,所以用戶的身份無法得到驗證,從而產生了不一致的問題。
解決這個問題的辦法有很多,比如可以協調多個服務器,讓他們的session保持同步。也可以在分發請求時做綁定處理,即將某一個IP固定分配給同一個服務器。但這些方式都比較麻煩,而且性能上也有一定的消耗。更合理的方式就是采用類似于Redis這樣的高性能緩存服務器,來實現分布式session。
從上面的敘述可知,我們使用session保存用戶的身份信息,本質上是要做兩件事情。第一是保存用戶的身份信息,第二是驗證用戶的身份信息。如果利用其它手段實現這兩個目標,那么就可以不用session,或者說我們使用的是廣義上的session了。
具體實現的思路如下圖,我們在服務端增加兩段程序:
第一是創建令牌的程序,就是在用戶初次訪問服務器時,給它創建一個唯一的身份標識,并且使用cookie封裝這個標識再發送給客戶端。那么當客戶端下次再訪問服務器時,就會自動攜帶這個身份標識了,這和SESSIONID的道理是一樣的,只是改由我們自己來實現了。另外,在返回令牌之前,我們需要將它存儲起來,以便于后續的驗證。而這個令牌是不能保存在服務器本地的,因為其他服務器無法訪問它。因此,我們可以將其存儲在服務器之外的一個地方,那么Redis便是一個理想的場所。
第二是驗證令牌的程序,就是在用戶再次訪問服務器時,我們獲取到了它之前的身份標識,那么我們就要驗證一下這個標識是否存在了。驗證的過程很簡單,我們從Redis中嘗試獲取一下就可以知道結果。
1.25 如何利用Redis實現一個分布式鎖?
參考答案
何時需要分布式鎖?
在分布式的環境下,當多個server并發修改同一個資源時,為了避免競爭就需要使用分布式鎖。那為什么不能使用Java自帶的鎖呢?因為Java中的鎖是面向多線程設計的,它只局限于當前的JRE環境。而多個server實際上是多進程,是不同的JRE環境,所以Java自帶的鎖機制在這個場景下是無效的。
如何實現分布式鎖?
采用Redis實現分布式鎖,就是在Redis里存一份代表鎖的數據,通常用字符串即可。實現分布式鎖的思路,以及優化的過程如下:
加鎖:
第一版,這種方式的缺點是容易產生死鎖,因為客戶端有可能忘記解鎖,或者解鎖失敗。
setnx key value第二版,給鎖增加了過期時間,避免出現死鎖。但這兩個命令不是原子的,第二步可能會失敗,依然無法避免死鎖問題。
setnx key value expire key seconds第三版,通過“set…nx…”命令,將加鎖、過期命令編排到一起,它們是原子操作了,可以避免死鎖。
set key value nx ex seconds解鎖:
解鎖就是刪除代表鎖的那份數據。
del key問題:
看起來已經很完美了,但實際上還有隱患,如下圖。進程A在任務沒有執行完畢時,鎖已經到期被釋放了。等進程A的任務執行結束后,它依然會嘗試釋放鎖,因為它的代碼邏輯就是任務結束后釋放鎖。但是,它的鎖早已自動釋放過了,它此時釋放的可能是其他線程的鎖。
想要解決這個問題,我們需要解決兩件事情:
按照以上思路,優化后的命令如下:
# 加鎖 set key random-value nx ex seconds # 解鎖 if redis.call("get",KEYS[1]) == ARGV[1] thenreturn redis.call("del",KEYS[1]) elsereturn 0 end基于RedLock算法的分布式鎖:
上述分布式鎖的實現方案,是建立在單個主節點之上的。它的潛在問題如下圖所示,如果進程A在主節點上加鎖成功,然后這個主節點宕機了,則從節點將會晉升為主節點。若此時進程B在新的主節點上加鎖成果,之后原主節點重啟,成為了從節點,系統中將同時出現兩把鎖,這是違背鎖的唯一性原則的。
總之,就是在單個主節點的架構上實現分布式鎖,是無法保證高可用的。若要保證分布式鎖的高可用,則可以采用多個節點的實現方案。這種方案有很多,而Redis的官方給出的建議是采用RedLock算法的實現方案。該算法基于多個Redis節點,它的基本邏輯如下:
- 這些節點相互獨立,不存在主從復制或者集群協調機制;
- 加鎖:以相同的KEY向N個實例加鎖,只要超過一半節點成功,則認定加鎖成功;
- 解鎖:向所有的實例發送DEL命令,進行解鎖;
RedLock算法的示意圖如下,我們可以自己實現該算法,也可以直接使用Redisson框架。
1.26 說一說你對布隆過濾器的理解
參考答案
布隆過濾器可以用很低的代價,估算出數據是否真實存在。例如:給用戶推薦新聞時,要去掉重復的新聞,就可以利用布隆過濾器,判斷該新聞是否已經推薦過。
布隆過濾器的核心包括兩部分:
布隆過濾器的工作原理:
- 如果這幾個位置中,有一個位置的值是0,就說明這個布隆過濾器中,不存在這個key。
- 如果這幾個位置中,所有位置的值都是1,就說明這個布隆過濾器中,極有可能存在這個key。之所以不是百分之百確定,是因為也可能是其他的key運算導致該位置為1。
1.27 多臺Redis抗高并發訪問該怎么設計?
參考答案
Redis Cluster是Redis的分布式解決方案,在3.0版本正式推出,有效地解決了Redis分布式方面的需求。當遇到單機內存、并發、流量等瓶頸時,可以采用Cluster架構方案達到負載均衡的目的。
Redis集群采用虛擬槽分區來實現數據分片,它把所有的鍵根據哈希函數映射到0-16383整數槽內,計算公式為slot=CRC16(key)&16383,每一個節點負責維護一部分槽以及槽所映射的鍵值數據。虛擬槽分區具有如下特點:
Redis集群中數據的分片邏輯如下圖:
1.28 如果并發量超過30萬,怎么設計Redis架構?
參考答案
Redis Cluster是Redis的分布式解決方案,在3.0版本正式推出,有效地解決了Redis分布式方面的需求。當遇到單機內存、并發、流量等瓶頸時,可以采用Cluster架構方案達到負載均衡的目的。
Redis集群采用虛擬槽分區來實現數據分片,它把所有的鍵根據哈希函數映射到0-16383整數槽內,計算公式為slot=CRC16(key)&16383,每一個節點負責維護一部分槽以及槽所映射的鍵值數據。虛擬槽分區具有如下特點:
Redis集群中數據的分片邏輯如下圖:
,就說明這個布隆過濾器中,不存在這個key。
- 如果這幾個位置中,所有位置的值都是1,就說明這個布隆過濾器中,極有可能存在這個key。之所以不是百分之百確定,是因為也可能是其他的key運算導致該位置為1。
1.27 多臺Redis抗高并發訪問該怎么設計?
參考答案
Redis Cluster是Redis的分布式解決方案,在3.0版本正式推出,有效地解決了Redis分布式方面的需求。當遇到單機內存、并發、流量等瓶頸時,可以采用Cluster架構方案達到負載均衡的目的。
Redis集群采用虛擬槽分區來實現數據分片,它把所有的鍵根據哈希函數映射到0-16383整數槽內,計算公式為slot=CRC16(key)&16383,每一個節點負責維護一部分槽以及槽所映射的鍵值數據。虛擬槽分區具有如下特點:
Redis集群中數據的分片邏輯如下圖:
[外鏈圖片轉存中…(img-W0cUgnt5-1641471982130)]
1.28 如果并發量超過30萬,怎么設計Redis架構?
參考答案
Redis Cluster是Redis的分布式解決方案,在3.0版本正式推出,有效地解決了Redis分布式方面的需求。當遇到單機內存、并發、流量等瓶頸時,可以采用Cluster架構方案達到負載均衡的目的。
Redis集群采用虛擬槽分區來實現數據分片,它把所有的鍵根據哈希函數映射到0-16383整數槽內,計算公式為slot=CRC16(key)&16383,每一個節點負責維護一部分槽以及槽所映射的鍵值數據。虛擬槽分區具有如下特點:
Redis集群中數據的分片邏輯如下圖:
總結
以上是生活随笔為你收集整理的Redis常见面试题详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux和GNU之间的关系
- 下一篇: 视图机制对于数据库的安全意义