轉載于:https://mp.weixin.qq.com/s/qvXm1pU8T_2mCZCjkTR7QA
1 Redis常見面試問題
1.1 Redis是單線程還是多線程
Redis
不同版本之間采用的線程模型是不一樣的,在Redis4.0
版本之前使用的是單線程模型
,在4.0
版本之后增加了多線程的支持。
在4.0
之前雖然說Redis
是單線程,也只是說它的網絡I/O
線程以及Set
和 Get
操作是由一個線程完成的。但是Redis
的持久化
、集群同步
還是使用其他線程來完成。
4.0
之后引入了多線程
的支持,主要是體現在大數據的異步刪除
功能上,例如 unlink key、flushdb async、flushall async
等
隨著底層網絡硬件性能的提升,Redis
的性能瓶頸逐漸體現在網絡 I/O
的讀/寫上,單個線程處理網絡讀/寫的速度跟不上底層網絡硬件執行的速度。讀/寫網絡
的讀/寫系統調用占用了 Redis
執行期間大部分 CPU
時間,所以 Redis
采用多個 I/O 線程
來處理網絡請求
,提高網絡請求處理的并行度
不過Redis
從 6.0
版本開始正式宣布支持多線程模型
,需要注意的是,Redis
多 I/O
線程模型只用來處理網絡讀/寫請求
,Redis
的讀/寫命令
依然是單線程
處理的。
1.2 使用單線程原因
那為什么Redis在4.0之前會選擇使用單線程?而且使用單線程還那么快?
選擇單線程主要是使用簡單,不存在鎖競爭
,可以在無鎖的情況下完成所有操作,不存在死鎖和線程切換帶來的性能和時間上的開銷,但同時單線程也不能完全發揮出多核CPU性能
為什么單線程那么快主要有以下幾個原因:
-
Redis
的大部分操作都在內存中完成,內存中的執行效率本身就很快,并且采用了高效的數據結構,比如哈希表和跳表。 - 使用單線程避免了多線程的競爭,省去了多線程切換帶來的時間和性能開銷,并且不會出現死鎖。
- 采用
I/O
多路復用機制處理大量客戶端的Socket
請求,因為這是基于非阻塞的 I/O 模型,這就讓Redis
可以高效地進行網絡通信,I/O的讀寫流程也不再阻塞。 - 很多的客戶端連接先到linux中的內核,內核和redis中間使用的是epoll(非阻塞的多路復用),那些進程一筆一筆的進行的。
在分布式情況下,這個數據一致性很重要。每個連接里邊命令是順序到達、順序處理的,但是如果說里面有個key,這個key為a,那么兩個客戶端發了一個對a的操作,那么無論從網絡當中跳躍誰先到達的,或者指定誰先輪到誰了。那么其實這兩個人對一個的操作,很難判定是誰先誰后,但是如果是你一個人,它里邊線性,而且沒有使用多線程,線程還是安全的,雖然它可以有多線程,但是線程安全,對a的操作,這邊能控制住,先創建a再刪除a,只要這邊能操作的話,那么這個數據是可以保證的。如果是單線程,這個客戶端就是一個線程,就是一個socket里面也是一個線程,那么這個線程肯定是先發出一個創建a再發出一個刪除a,但是如果客戶端里邊是多線程,那么這里一個創建命令和刪除命令,指不定誰跑到前面了,如果線程不是安全的話,那么有可能先把刪除的發出去,再把創建的發出去。
點擊了解Linux中epoll原理機制
1.3 Redis高可用
Redis實現高可用主要有三種方式:主從復制
、哨兵模式
,以及 Redis 集群
1.3.1 主從復制
將從前的一臺 Redis
服務器,同步數據到多臺從 Redis
服務器上,即一主多從的模式,這個跟MySQL
主從復制的原理一樣。
點擊了解redis 持久化中的主從復制同步
1.3.2 哨兵模式
1.3.2.1 簡介
使用 Redis
主從復制的時候,會有一個問題,就是當 Redis
的主從服務器出現故障宕機時,需要手動進行恢復,為了解決這個問題,Redis
增加了哨兵模式(因為哨兵模式做到了可以監控主從服務器,并且提供自動容災恢復的功能)。
它專注于對 Redis 實例
(主節點、從節點)運行狀態的監控,并能夠在主節點發生故障時通過一系列的機制實現選主
及主從切換
,實現自動故障轉移,確保整個 Redis 系統的可用性。
sentinel
主要做四件事情:
-
監控
master
和slave
狀態,判斷是否下線。- 每秒一次的頻率向
master
和slave
以及其他sentinel
發送PING
命令,如果該節點距離最后一次響應 PING 的時間超過down-after-milliseconds
選項所指定的值(即在設定的時間內沒有回復心跳包), 則這個實例會被Sentinel
標記為主觀下線,當master
被標記主觀下線。 - 其他正在監視這個
master
的所有sentinel
會按照每秒一次的頻率確認master
是否主觀下線。 - 當超過配置的
quorum
數量的sentinel
都認為master
主觀下線,則標記這個master
客觀下線。
- 每秒一次的頻率向
-
選舉新
master
,如果 master 出現故障,sentine 需要選舉一個 slave 晉升為新 master。晉升為新 master 的 slave 是有條件的,先過濾不滿足條件的,再打分排優先級。- 過濾掉下線、網絡異常的
slave
。 - 過濾掉經常與
master
斷開的slave
。 -
slave
優先級,通過replica-priority 100
配置,值越高,優先級越高。 - 若優先級相同,則復制偏移量(
processed replication offset
)最大的從節點,已復制的數據量越多越好,slave_repl_offset
與master_repl_offset
差值越小。 -
slave runID
,在優先級和復制進度都相同的情況下,runID
最小的 slave 得分最高,會被選為新主庫。
- 過濾掉下線、網絡異常的
-
選舉領導者哨兵(
Leader Sentinel
),執行主從切換,從sentinel
集群中選舉一個leader
執行故障自動切換。
成為leader
的條件是收到足夠的贊成票(大于等于quorum
和(總節點數/2 + 1
)的最大值)。
第一個判定master
主觀下線的sentinel
收到其他sentinel
節點的回復并確定master
客觀下線后,就會給其他sentinel
節點發送命令申請成為 leader。
選舉領導者哨兵而不是直接從從節點中選舉新的主節點,主要是為了以下原因:- 協調一致性:通過選舉領導者哨兵,可以確保在集群中僅有一個哨兵負責主從切換,避免多個哨兵同時進行切換操作導致的不一致性。
- 集中決策:領導者哨兵集中管理整個主從切換過程,使得切換過程更加有序和可控。
- 分擔職責:哨兵負責監控和管理 Redis 節點,而從節點主要用于數據復制和故障切換。在發生故障時,選舉領導者哨兵來管理切換過程,可以讓從節點專注于數據同步,分擔系統負載
-
通知,通知其他
slave
執行replicaof
與新的master
同步數據,并通知客戶端與新 master 建立連接。
image.png 哨兵如何通知客戶端?
選出新主節點后,哨兵的任務還沒完。它需要告訴所有從節點
和客戶端
:主節點已經更換
對于從節點,哨兵會自動修改它們的復制目標,對于客戶端,哨兵會通過發布訂閱通知它們新的主節點地址。
哨兵會通過Redis
的發布訂閱
系統,把新主節點的信息推送給所有訂閱的客戶端。
例如,使用SENTINEL get-master-addr-by-name mymaster
命令可以獲取新主節點的 IP 和端口。
redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster
# 返回新主節點的 IP 和端口,比如 [ "127.0.0.1", "6380" ]
1.3.2.2 哨兵機制的高可用性保障
一個哨兵單點可能會掛掉,所以 Redis
支持多哨兵
協同工作。這種模式下,哨兵之間也會相互通信,通過 Raft
類似的選舉機制選出一個領頭哨兵
(Leader Sentinel
),由它負責協調故障轉移。
每個哨兵實例都可以彼此通信,確保在任何一個哨兵掛掉的情況下,系統仍然能正常工作。
哨兵機制是 Redis
提供的高可用
解決方案,主要功能包括監控主從節點狀態、進行故障轉移以及通知客戶端更新主節點信息。
-
監控
:哨兵通過PING
命令定期檢測主從節點是否存活,基于響應時間判斷節點狀態。 -
選主
:哨兵根據復制偏移量、優先級和網絡延遲等標準選舉新的主節點,保證數據一致性和快速恢復。 -
高可用性
:通過多個哨兵協同工作,避免單點故障。 -
通知
:哨兵利用發布訂閱機制通知客戶端主節點變更,確保業務無縫切換。
1.3.3 Redis Cluster(集群)
哨兵模式
基于主從模式,實現讀寫分離,它還可以自動切換,系統可用性更高。但是它每個節點存儲的數據是一樣的,浪費內存,并且不好在線擴容。因此,Reids Cluster
集群(切片集群的實現方案)應運而生,它在Redis3.0加入的,實現了Redis的分布式存儲。對數據進行分片,也就是說每臺Redis節點上存儲不同的內容,來解決在線擴容的問題。并且,它可以保存大量數據,即分散數據到各個Redis實例,還提供復制和故障轉移的功能。
Redis Cluster
是一種分布式去中心化的運行模式,是在 Redis 3.0 版本中推出的 Redis 集群方案,通過分片(sharding
)來進行數據管理(分治思想的一種實踐),并提供復制和故障轉移功能,它將數據分布在不同的服務器上,以此來降低系統對單主節點的依賴,從而提高 Redis
服務的讀寫性能。
使用哨兵模式在數據上有副本數據做保證,在可用性上又有哨兵監控,一旦master
宕機會選舉salve
節點為master
節點,那為什么還需要使用集群模式呢?
哨兵模式歸根節點還是主從模式
,在主從模式下我們可以通過增加salve
節點來擴展讀并發能力,但是沒辦法擴展寫能力和存儲能力,存儲能力只能是master
節點能夠承載的上限。所以為了擴展寫能力和存儲能力,我們就需要引入集群模式。
集群中那么多Master
節點,redis cluster
在存儲的時候如何確定選擇哪個節點呢?
Redis Cluster
采用的是數據分片
實現節點選擇的
1.4 Redis內存(數據)淘汰策略
在redis
中,我們是可以去設置最大使用內存大小server.maxmemory
的,當redis
內存數據集大小上升到一定程度的時候,就會施行數據淘汰機制。
不同位數的操作系統,maxmemory
的默認值是不同的:
- 在 64 位操作系統中,
maxmemory
的默認值是 0,表示沒有內存大小限制,那么不管用戶存放多少數據到Redis
中,Redis
也不會對可用內存進行檢查,直到 Redis 實例因內存不足而崩潰也無作為。 - 在 32 位操作系統中,
maxmemory
的默認值是3G
,因為 32 位的機器最大只支持4GB
的內存,而系統本身就需要一定的內存資源來支持運行,所以 32 位操作系統限制最大 3 GB 的可用內存是非常合理的,這樣可以避免因為內存不足而導致Redis
實例崩潰。
Redis
提供了8種數據淘汰策略,分為不進行數據淘汰
和進行數據淘汰
兩類策略,
不進行數據淘汰的策略 noeviction
(Redis3.0
之后,默認的內存淘汰策略),它表示當運行內存超過最大設置內存時,不淘汰任何數據,這時如果有新的數據寫入,會報錯通知禁止寫入,不淘汰任何數據,但是如果沒用數據寫入的話,只是單純的查詢或者刪除操作的話,還是可以正常工作。
-
no-enviction
:禁止淘汰數據,如果redis寫滿了將不提供寫請求,直接返回錯誤
進行數據淘汰的策略:
-
volatile-lru
:從已經設置過期時間的數據集中,挑選最近最少使用的數據淘汰,即:最久未使用的鍵值 -
volatile-ttl
:從已經設置過期時間的數據集中,挑選即將要過期的數據淘汰。 -
volatile-random
:從已經設置過期時間的數據集中,隨機挑選數據淘汰。 -
volatile-lfu
:從已經設置過期時間的數據集中,會使用LFU
算法選擇設置了過期時間的鍵值對,即:最不常用的鍵值 -
allkeys-lru
:從所有的數據集中,挑選最近最少使用的數據淘汰。 -
allkeys-random
:從所有的數據集中,隨機挑選數據淘汰。 -
allkeys-lfu
:淘汰整個鍵值中最不常用的鍵值
附錄:LRU
和LFU
是不同的:
-
LRU
是最近最少使用頁面置換算法(Least Recently Used
),也就是首先淘汰最長時間未被使用的頁面 -
LFU
是最近最不常用頁面置換算法(Least Frequently Used
),也就是淘汰一定時期內被訪問次數最少的頁
使用策略規則:
- 如果數據呈現
冪律分布
,也就是一部分數據訪問頻率高,一部分數據訪問頻率低,則使用allkeys-lru
- 如果數據呈現
平等分布
,也就是所有的數據訪問頻率都相同,則使用allkeys-random
1.5 Redis過期鍵刪除策略
Redis
過期鍵刪除策略:
-
定時刪除
:在設置鍵的過期時間的同時,創建一個timer
,讓定時器在鍵的過期時間到達時,立即執行對鍵的刪除操作。(主動刪除)
對內存友好,但是對cpu時間不友好,有較多過期鍵的而情況下,刪除過期鍵會占用相當一部分cpu時間。 -
惰性刪除
:放任過期鍵不管,但是每次從鍵空間中獲取鍵時,都檢查取到的鍵是否過去,如果過期就刪除,如果沒過期就返回該鍵。(被動刪除)
對cpu
時間友好,程序只會在取出鍵的時候才會對鍵進行過期檢查,這不會在刪除其他無關過期鍵上花費任何cpu時間,但是如果一個鍵已經過期,而這個鍵又保留在數據庫中,那么只要這個過期鍵不被刪除,他所占用的內存就不會釋放,對內存不友好。
這種方式在Redis中主要是通過expireIfNeeded
函數來實現的
在Redis 4.0
之后引入的,叫做lazyfree_lazy_expire
,它決定了是否異步刪除過期鍵
。如果設置為1
,表示使用異步刪除,避免阻塞其他操作;如果設置為0
,則會同步刪除,刪除操作會阻塞當前請求,直到刪除完成 -
定期刪除
:每隔一段時間就對數據庫進行一次檢查,刪除里面的過期鍵。(主動刪除)采用對內存
和cpu
時間折中的方法,每隔一段時間就對一些key
進行采樣檢查,檢查是否過期,如果過期就進行刪除
定期刪除的檢查周期默認是每秒執行10次
,可以通過hz
配置來調整。默認情況下,hz值為10
。
定期刪除的邏輯是這樣的:- 采樣一定個數的
key
(默認20
),采樣的個數可以進行配置,并將其中過期的key
全部刪除; - 如果過期
key
的占比超過可接受的過期key
的百分比(默認25%
),則重復刪除的過程,直到過期key
的比例降至可接受的過期key
的百分比以下 -
注意
:為了防止定期刪除過程中的循環過長
,Redis
限制了每輪檢查的最大時間,默認情況下不會超過25毫秒
。如果在25毫秒
內沒有完成過期鍵的刪除,Redis
會強制退出當前輪次,等到下一輪繼續處理。
- 采樣一定個數的
1.6 Redis的key和value可以存儲的最大值分別是多少
雖然Key
的大小上限為512M
,但是一般建議key
的大小不要超過1KB
,這樣既可以節約存儲空間,又有利于Redis
進行檢索。
value
的最大值也是512M
。對于String
類型的value
值上限為512M
,而集合、鏈表、哈希等key
類型,單個元素的value
上限也為512M
1.7 Redis實現數據的去重
-
Redis
的set
:它可以去除重復元素,也可以快速判斷某一個元素是否存在于集合中,如果元素很多(比如上億的計數),占用內存很大。 -
Redis
的bit
:它可以用來實現比set內存高度壓縮的計數,它通過一個bit
設置為1
或者0
,表示存儲某個元素是否存在信息。例如網站唯一訪客計數,可以把user_id
作為bit
的偏移量offset
,如設置為1
表示有訪問,使用1 MB
的空間就可以存放800多萬用戶的一天訪問計數情況。 -
HyperLogLog
:實現超大數據量精確的唯一計數都是比較困難的,HyperLogLog
可以僅僅使用 12 k左右的內存,實現上億的唯一計數,而且誤差控制在百分之一左右。 -
bloomfilter
布隆過濾器:布隆過濾器是一種占用空間很小的數據結構,它由一個很長的二進制向量和一組Hash映射函數組成,它用于檢索一個元素是否在一個集合中
1.8 Redis序列化
Redis
什么時候需要序列化?
-
序列化
:將Java
對象轉換成字節流的過程。 -
反序列化
:將字節流轉換成Java
對象的過程。
為什么需要序列化呢?
打個比喻:作為大城市漂泊的碼農,搬家是常態。當我們搬書桌時,桌子太大了就通不過比較小的門,因此我們需要把它拆開再搬過去,這個拆桌子的過程就是序列化。而我們把書桌復原回來(安裝)的過程就是反序列化啦。
比如想把內存中的對象狀態保存到一個文件中或者數據庫中的時候(最常用,如保存到redis
);再比喻想用套接字在網絡上傳送對象的時候,都需要序列化。
RedisSerializer
接口 是 Redis
序列化接口,用于 Redis KEY
和 VALUE
的序列化,有如下序列化方式:
-
JDK
序列化方式 (默認) -
String
序列化方式 -
JSON
序列化方式 -
XML
序列化方式
1.9 大key
1.9.1 定義
Redis
中的 大key
是指存儲在Redis
中的占用內存較大的鍵值對。大key
可能會導致Redis
的性能下降,因為大key
占用的內存較多,需要較長的時間來進行讀寫操作。而且,當大key
被刪除時,會阻塞Redis
的其他操作。
常見的大key包括:
- 存儲大量數據的字符串類型鍵值對。
- 存儲大量元素的列表、集合或有序集合。
- 包含大量字段的哈希表。
1.9.2 大Key解決方案
為了避免大key
對Redis
性能的影響,可以采取以下措施:
- 將
大key
拆分為多個較小的鍵值對,以減少每個鍵值對的內存占用。 - 使用分布式緩存,將
大key
分散到多個Redis
實例上。 - 使用壓縮算法對
大key
進行壓縮,減少內存占用。 - 使用
Redis
的分片功能,將大key
分散到多個分片上,減少單個Redis
實例的負載。 - 對
大Key
進行清理。將不適用Redis能力的數據存至其它存儲,并在Redis中刪除此類數據。注意,要使用異步刪除。 - 監控Redis的內存水位。可以通過監控系統設置合理的Redis內存報警閾值進行提醒,例如Redis內存使用率超過70%、Redis的內存在1小時內增長率超過20%等。
- 對過期數據進行定期清。堆積大量過期數據會造成
大Ke
y的產生,例如在HASH數據類型中以增量的形式不斷寫入大量數據而忽略了數據的時效性??梢酝ㄟ^定時任務的方式對失效數據進行清理。
1.10 熱Key
1.10.1 定義
Redis熱key
是指在Redis
中被頻繁訪問的鍵。當某個鍵被頻繁訪問時,它就被認為是熱key
。熱key
通常是由于某些熱門操作、熱門數據或者高并發訪問所導致的
通常以其接收到的 Key
被請求頻率來判定,例如:
-
QPS
集中在特定的Key:Redis實例的總QPS(每秒查詢率)為10,000,而其中一個Key的每秒訪問量達到了7,000。 - 帶寬使用率集中在特定的Key:對一個擁有上千個成員且總大小為1 MB的HASH Key每秒發送大量的HGETALL操作請求。
- CPU使用時間占比集中在特定的Key:對一個擁有數萬個成員的Key(ZSET類型)每秒發送大量的ZRANGE操作請求。
1.10.2 如何解決熱key
熱key
可能對Redis
的性能產生重大影響。當一個鍵被頻繁訪問時,Redis
需要頻繁地從內存中讀取或寫入該鍵的值,這可能導致內存帶寬的瓶頸、CPU利用率的增加以及延遲的增加。此外,如果一個熱key的值過大,可能會占用大量的內存,進一步影響Redis的性能
解決方案:
- 在
Redis
集群架構中對熱Key
進行復制,數據分片
在Redis
集群架構中,由于熱Key
的遷移粒度問題,無法將請求分散至其他數據分片,導致單個數據分片的壓力無法下降。此時,可以將對應熱Key進行復制并遷移至其他數據分片,例如將熱Key foo復制出3個內容完全一樣的Key并名為foo2、foo3、foo4,將這三個Key遷移到其他數據分片來解決單個數據分片的熱Key壓力。 - 使用讀寫分離架構。
如果熱Key
的產生來自于讀請求,您可以將實例改造成讀寫分離架構來降低每個數據分片的讀請求壓力,甚至可以不斷地增加從節點。但是讀寫分離架構在增加業務代碼復雜度的同時,也會增加Redis集群架構復雜度。不僅要為多個從節點提供轉發層(如Proxy,LVS等)來實現負載均衡,還要考慮從節點數量顯著增加后帶來故障率增加的問題。Redis集群架構變更會為監控、運維、故障處理帶來了更大的挑戰。 - 使用合適的數據結構
根據實際情況選擇合適的數據結構,如列表、集合、有序集合等,來存儲熱key的值,以提高讀寫性能。 - 緩存策略
使用合理的緩存策略,比如設置合適的過期時間、使用LRU算法等,以減少對熱key的訪問頻率。 - 持久化策略
根據業務需求選擇合適的持久化方式,如RDB快照、AOF日志等,以確保數據的安全性和可靠性。
1.11 Redis中緩沖區
Redis中的緩沖區主要根據其功能和用途進行劃分,可以歸納為以下幾種:
- 客戶端緩沖區:
- 輸入緩沖區:緩存客戶端發送過來的命令,
Redis
主線程從該緩沖區中讀取命令進行處理。 - 輸出緩沖區:當
Redis
主線程處理完數據后,將結果寫入該緩沖區,再返回給客戶端。 - 緩存區溢出原因:
- 寫入了BigKey,即一次性寫入了大量數據,超過了緩沖區的大小。
- 服務端處理請求的速度過慢,導致無法及時處理請求,使得客戶端發送的請求在緩沖區內越積越多。
- 輸入緩沖區:緩存客戶端發送過來的命令,
- 復制緩沖區:
- 復制緩沖區:在全量復制過程中,主節點在向從節點傳輸
RDB
文件的同時,會繼續接收客戶端發送的寫命令請求。這些寫命令會先保存在復制緩沖區中,等RDB
文件傳輸完成后,再發送給從節點去執行。 - 復制積壓緩沖區:在增量復制時,主節點和從節點進行常規同步時,會把寫命令暫存在
復制積壓緩沖區
中。 - 溢出原因:
- 主庫傳輸RDB文件以及從庫加載RDB文件耗時長,同時主庫接收的寫命令操作較多。
- 緩沖區大小設置不合理。
- 復制緩沖區:在全量復制過程中,主節點在向從節點傳輸
- AOF緩沖區:
- AOF緩沖區:當
Redis
進行持久化時,會先將客戶端傳來的命令存放在AOF
緩沖區,再根據具體的策略(always、everysec、no)去寫入磁盤中的AOF文件中。 - AOF重寫緩沖區:在
Redis
進行AOF
重寫時,主進程會fork一個子進程進行AOF
重寫,此時主進程接收的指令會存放在AOF重寫緩沖區中。當AOF重寫完成后,這些指令會被追加到AOF文件中。
- AOF緩沖區:當
- 內存級緩存:
雖然不是嚴格意義上的緩沖區
,但Redis
的內存級緩存是其最常見的使用場景。通過將數據存儲在內存中,減少讀取數據庫的頻率,提高數據訪問速度。 - 其他內存使用:
-
Redis
空進程自身內存消耗非常少,通常used_memory_rss
在3MB左右,used_memory在800KB左右。 - 對象內存:存儲著用戶所有的數據,消耗可以簡單理解為sizeof(keys)+sizeof(values)。
- 內存碎片:正常的碎片率在1.03左右
-