您好,登錄后才能下訂單哦!
這篇文章主要講解了“Redis經典面試題及答案有哪些”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“Redis經典面試題及答案有哪些”吧!
Redis,英文全稱是Remote Dictionary Server(遠程字典服務),是一個開源的使用ANSI C語言編寫、支持網絡、可基于內存亦可持久化的日志型、Key-Value數據庫,并提供多種語言的API。
與MySQL數據庫不同的是,Redis的數據是存在內存中的。它的讀寫速度非常快,每秒可以處理超過10萬次讀寫操作。因此redis被廣泛應用于緩存,另外,Redis也經常用來做分布式鎖。除此之外,Redis支持事務、持久化、LUA 腳本、LRU 驅動事件、多種集群方案。
大多數小伙伴都知道,Redis有以下這五種基本類型:
String(字符串)
Hash(哈希)
List(列表)
Set(集合)
zset(有序集合)
它還有三種特殊的數據結構類型
Geospatial
Hyperloglog
Bitmap
String(字符串)
簡介:String是Redis最基礎的數據結構類型,它是二進制安全的,可以存儲圖片或者序列化的對象,值最大存儲為512M
簡單使用舉例: set key value
、get key
等
應用場景:共享session、分布式鎖,計數器、限流。
內部編碼有3種,int(8字節長整型)/embstr(小于等于39字節字符串)/raw(大于39個字節字符串)
C語言的字符串是char[]
實現的,而Redis使用SDS(simple dynamic string) 封裝,sds源碼如下:
struct sdshdr{
unsigned int len; // 標記buf的長度
unsigned int free; //標記buf中未使用的元素個數
char buf[]; // 存放元素的坑
}
SDS 結構圖如下:
Redis為什么選擇SDS結構,而C語言原生的char[]
不香嗎?
舉例其中一點,SDS中,O(1)時間復雜度,就可以獲取字符串長度;而C 字符串,需要遍歷整個字符串,時間復雜度為O(n)
Hash(哈希)
簡介:在Redis中,哈希類型是指v(值)本身又是一個鍵值對(k-v)結構
簡單使用舉例:hset key field value
、hget key field
內部編碼:ziplist(壓縮列表)
、hashtable(哈希表)
應用場景:緩存用戶信息等。
注意點:如果開發使用hgetall,哈希元素比較多的話,可能導致Redis阻塞,可以使用hscan。而如果只是獲取部分field,建議使用hmget。
字符串和哈希類型對比如下圖:
List(列表)
簡介:列表(list)類型是用來存儲多個有序的字符串,一個列表最多可以存儲2^32-1個元素。
簡單實用舉例:lpush key value [value ...]
、lrange key start end
內部編碼:ziplist(壓縮列表)、linkedlist(鏈表)
應用場景:消息隊列,文章列表,
一圖看懂list類型的插入與彈出:
list應用場景參考以下:
lpush+lpop=Stack(棧)
lpush+rpop=Queue(隊列)
lpsh+ltrim=Capped Collection(有限集合)
lpush+brpop=Message Queue(消息隊列)
Set(集合)
簡介:集合(set)類型也是用來保存多個的字符串元素,但是不允許重復元素
簡單使用舉例:sadd key element [element ...]
、smembers key
內部編碼:intset(整數集合)
、hashtable(哈希表)
注意點:smembers和lrange、hgetall都屬于比較重的命令,如果元素過多存在阻塞Redis的可能性,可以使用sscan來完成。
應用場景:用戶標簽,生成隨機數抽獎、社交需求。
有序集合(zset)
簡介:已排序的字符串集合,同時元素不能重復
簡單格式舉例:zadd key score member [score member ...]
,zrank key member
底層內部編碼:ziplist(壓縮列表)
、skiplist(跳躍表)
應用場景:排行榜,社交需求(如用戶點贊)。
Geo:Redis3.2推出的,地理位置定位,用于存儲地理位置信息,并對存儲的信息進行操作。
HyperLogLog:用來做基數統計算法的數據結構,如統計網站的UV。
Bitmaps :用一個比特位來映射某個元素的狀態,在Redis中,它的底層是基于字符串類型實現的,可以把bitmaps成作一個以比特位為單位的數組
Redis為什么這么快
我們都知道內存讀寫是比在磁盤快很多的,Redis基于內存存儲實現的數據庫,相對于數據存在磁盤的MySQL數據庫,省去磁盤I/O的消耗。
我們知道,Mysql索引為了提高效率,選擇了B+樹的數據結構。其實合理的數據結構,就是可以讓你的應用/程序更快。先看下Redis的數據結構&內部編碼圖:
SDS簡單動態字符串
字符串長度處理:Redis獲取字符串長度,時間復雜度為O(1),而C語言中,需要從頭開始遍歷,復雜度為O(n);
空間預分配:字符串修改越頻繁的話,內存分配越頻繁,就會消耗性能,而SDS修改和空間擴充,會額外分配未使用的空間,減少性能損耗。
惰性空間釋放:SDS 縮短時,不是回收多余的內存空間,而是free記錄下多余的空間,后續有變更,直接使用free中記錄的空間,減少分配。
二進制安全:Redis可以存儲一些二進制數據,在C語言中字符串遇到'\0'會結束,而 SDS中標志字符串結束的是len屬性。
字典
Redis 作為 K-V 型內存數據庫,所有的鍵值就是用字典來存儲。字典就是哈希表,比如HashMap,通過key就可以直接獲取到對應的value。而哈希表的特性,在O(1)時間復雜度就可以獲得對應的值。
跳躍表
跳躍表是Redis特有的數據結構,就是在鏈表的基礎上,增加多級索引提升查找效率。
跳躍表支持平均 O(logN),最壞 O(N)復雜度的節點查找,還可以通過順序性操作批量處理節點。
Redis 支持多種數據數據類型,每種基本類型,可能對多種數據結構。什么時候,使用什么樣數據結構,使用什么樣編碼,是redis設計者總結優化的結果。
String:如果存儲數字的話,是用int類型的編碼;如果存儲非數字,小于等于39字節的字符串,是embstr;大于39個字節,則是raw編碼。
List:如果列表的元素個數小于512個,列表每個元素的值都小于64字節(默認),使用ziplist編碼,否則使用linkedlist編碼
Hash:哈希類型元素個數小于512個,所有值小于64字節的話,使用ziplist編碼,否則使用hashtable編碼。
Set:如果集合中的元素都是整數且元素個數小于512個,使用intset編碼,否則使用hashtable編碼。
Zset:當有序集合的元素個數小于128個,每個元素的值小于64字節時,使用ziplist編碼,否則使用skiplist(跳躍表)編碼
I/O 多路復用
I/O 多路復用
多路I/O復用技術可以讓單個線程高效的處理多個連接請求,而Redis使用用epoll作為I/O多路復用技術的實現。并且,Redis自身的事件處理模型將epoll中的連接、讀寫、關閉都轉換為事件,不在網絡I/O上浪費過多的時間。
什么是I/O多路復用?
I/O :網絡 I/O
多路 :多個網絡連接
復用:復用同一個線程。
IO多路復用其實就是一種同步IO模型,它實現了一個線程可以監視多個文件句柄;一旦某個文件句柄就緒,就能夠通知應用程序進行相應的讀寫操作;而沒有文件句柄就緒時,就會阻塞應用程序,交出cpu。
單線程模型
Redis是單線程模型的,而單線程避免了CPU不必要的上下文切換和競爭鎖的消耗。也正因為是單線程,如果某個命令執行過長(如hgetall命令),會造成阻塞。Redis是面向快速執行場景的數據庫。,所以要慎用如smembers和lrange、hgetall等命令。
Redis 6.0 引入了多線程提速,它的執行命令操作內存的仍然是個單線程。
Redis直接自己構建了VM機制 ,不會像一般的系統會調用系統函數處理,會浪費一定的時間去移動和請求。
Redis的虛擬內存機制是啥呢?
虛擬內存機制就是暫時把不經常訪問的數據(冷數據)從內存交換到磁盤中,從而騰出寶貴的內存空間用于其它需要訪問的數據(熱數據)。通過VM功能可以實現冷熱數據分離,使熱數據仍在內存中、冷數據保存到磁盤。這樣就可以避免因為內存不足而造成訪問速度下降的問題。
先來看一個常見的緩存使用方式:讀請求來了,先查下緩存,緩存有值命中,就直接返回;緩存沒命中,就去查數據庫,然后把數據庫的值更新到緩存,再返回。
讀取緩存
緩存穿透:指查詢一個一定不存在的數據,由于緩存是不命中時需要從數據庫查詢,查不到數據則不寫入緩存,這將導致這個不存在的數據每次請求都要到數據庫去查詢,進而給數據庫帶來壓力。
通俗點說,讀請求訪問時,緩存和數據庫都沒有某個值,這樣就會導致每次對這個值的查詢請求都會穿透到數據庫,這就是緩存穿透。
緩存穿透一般都是這幾種情況產生的:
業務不合理的設計,比如大多數用戶都沒開守護,但是你的每個請求都去緩存,查詢某個userid查詢有沒有守護。
業務/運維/開發失誤的操作,比如緩存和數據庫的數據都被誤刪除了。
黑客非法請求攻擊,比如黑客故意捏造大量非法請求,以讀取不存在的業務數據。
如何避免緩存穿透呢? 一般有三種方法。
1.如果是非法請求,我們在API入口,對參數進行校驗,過濾非法值。
2.如果查詢數據庫為空,我們可以給緩存設置個空值,或者默認值。但是如有有寫請求進來的話,需要更新緩存哈,以保證緩存一致性,同時,最后給緩存設置適當的過期時間。(業務上比較常用,簡單有效)
3.使用布隆過濾器快速判斷數據是否存在。即一個查詢請求過來時,先通過布隆過濾器判斷值是否存在,存在才繼續往下查。
布隆過濾器原理:它由初始值為0的位圖數組和N個哈希函數組成。一個對一個key進行N個hash算法獲取N個值,在比特數組中將這N個值散列后設定為1,然后查的時候如果特定的這幾個位置都為1,那么布隆過濾器判斷該key存在。
緩存雪奔: 指緩存中數據大批量到過期時間,而查詢數據量巨大,請求都直接訪問數據庫,引起數據庫壓力過大甚至down機。
緩存雪奔一般是由于大量數據同時過期造成的,對于這個原因,可通過均勻設置過期時間解決,即讓過期時間相對離散一點。如采用一個較大固定值+一個較小的隨機值,5小時+0到1800秒醬紫。
Redis 故障宕機也可能引起緩存雪奔。這就需要構造Redis高可用集群啦。
緩存擊穿: 指熱點key在某個時間點過期的時候,而恰好在這個時間點對這個Key有大量的并發請求過來,從而大量的請求打到db。
緩存擊穿看著有點像,其實它兩區別是,緩存雪奔是指數據庫壓力過大甚至down機,緩存擊穿只是大量并發請求到了DB數據庫層面。可以認為擊穿是緩存雪奔的一個子集吧。有些文章認為它倆區別,是區別在于擊穿針對某一熱點key緩存,雪奔則是很多key。
解決方案就有兩種:
1.使用互斥鎖方案。緩存失效時,不是立即去加載db數據,而是先使用某些帶成功返回的原子操作命令,如(Redis的setnx)去操作,成功的時候,再去加載db數據庫數據和設置緩存。否則就去重試獲取緩存。
2. “永不過期”,是指沒有設置過期時間,但是熱點數據快要過期時,異步線程去更新和設置過期時間。
什么是熱Key呢?在Redis中,我們把訪問頻率高的key,稱為熱點key。
如果某一熱點key的請求到服務器主機時,由于請求量特別大,可能會導致主機資源不足,甚至宕機,從而影響正常的服務。
而熱點Key是怎么產生的呢?主要原因有兩個:
用戶消費的數據遠大于生產的數據,如秒殺、熱點新聞等讀多寫少的場景。
請求分片集中,超過單Redi服務器的性能,比如固定名稱key,Hash落入同一臺服務器,瞬間訪問量極大,超過機器瓶頸,產生熱點Key問題。
那么在日常開發中,如何識別到熱點key呢?
憑經驗判斷哪些是熱Key;
客戶端統計上報;
服務代理層上報
如何解決熱key問題?
Redis集群擴容:增加分片副本,均衡讀流量;
將熱key分散到不同的服務器中;
使用二級緩存,即JVM本地緩存,減少Redis的讀請求。
我們在set key
的時候,可以給它設置一個過期時間,比如expire key 60
。指定這key60s后過期,60s后,redis是如何處理的嘛?我們先來介紹幾種過期策略:
定時過期
每個設置過期時間的key都需要創建一個定時器,到過期時間就會立即對key進行清除。該策略可以立即清除過期的數據,對內存很友好;但是會占用大量的CPU資源去處理過期的數據,從而影響緩存的響應時間和吞吐量。
惰性過期
只有當訪問一個key時,才會判斷該key是否已過期,過期則清除。該策略可以最大化地節省CPU資源,卻對內存非常不友好。極端情況可能出現大量的過期key沒有再次被訪問,從而不會被清除,占用大量內存。
定期過期
每隔一定的時間,會掃描一定數量的數據庫的expires字典中一定數量的key,并清除其中已過期的key。該策略是前兩者的一個折中方案。通過調整定時掃描的時間間隔和每次掃描的限定耗時,可以在不同情況下使得CPU和內存資源達到最優的平衡效果。
expires字典會保存所有設置了過期時間的key的過期時間數據,其中,key是指向鍵空間中的某個鍵的指針,value是該鍵的毫秒精度的UNIX時間戳表示的過期時間。鍵空間是指該Redis集群中保存的所有鍵。
Redis中同時使用了惰性過期和定期過期兩種過期策略。
假設Redis當前存放30萬個key,并且都設置了過期時間,如果你每隔100ms就去檢查這全部的key,CPU負載會特別高,最后可能會掛掉。
因此,redis采取的是定期過期,每隔100ms就隨機抽取一定數量的key來檢查和刪除的。
但是呢,最后可能會有很多已經過期的key沒被刪除。這時候,redis采用惰性刪除。在你獲取某個key的時候,redis會檢查一下,這個key如果設置了過期時間并且已經過期了,此時就會刪除。
但是呀,如果定期刪除漏掉了很多過期的key,然后也沒走惰性刪除。就會有很多過期key積在內存內存,直接會導致內存爆的。或者有些時候,業務量大起來了,redis的key被大量使用,內存直接不夠了,運維小哥哥也忘記加大內存了。難道redis直接這樣掛掉?不會的!Redis用8種內存淘汰策略保護自己~
volatile-lru:當內存不足以容納新寫入數據時,從設置了過期時間的key中使用LRU(最近最少使用)算法進行淘汰;
allkeys-lru:當內存不足以容納新寫入數據時,從所有key中使用LRU(最近最少使用)算法進行淘汰。
volatile-lfu:4.0版本新增,當內存不足以容納新寫入數據時,在過期的key中,使用LFU算法進行刪除key。
allkeys-lfu:4.0版本新增,當內存不足以容納新寫入數據時,從所有key中使用LFU算法進行淘汰;
volatile-random:當內存不足以容納新寫入數據時,從設置了過期時間的key中,隨機淘汰數據;。
allkeys-random:當內存不足以容納新寫入數據時,從所有key中隨機淘汰數據。
volatile-ttl:當內存不足以容納新寫入數據時,在設置了過期時間的key中,根據過期時間進行淘汰,越早過期的優先被淘汰;
noeviction:默認策略,當內存不足以容納新寫入數據時,新寫入操作會報錯。
緩存
排行榜
計數器應用
共享Session
分布式鎖
社交網絡
消息隊列
位操作
我們一提到redis,自然而然就想到緩存,國內外中大型的網站都離不開緩存。合理的利用緩存,比如緩存熱點數據,不僅可以提升網站的訪問速度,還可以降低數據庫DB的壓力。并且,Redis相比于memcached,還提供了豐富的數據結構,并且提供RDB和AOF等持久化機制,強的一批。
當今互聯網應用,有各種各樣的排行榜,如電商網站的月度銷量排行榜、社交APP的禮物排行榜、小程序的投票排行榜等等。Redis提供的zset
數據類型能夠實現這些復雜的排行榜。
比如,用戶每天上傳視頻,獲得點贊的排行榜可以這樣設計:
1.用戶Jay上傳一個視頻,獲得6個贊,可以醬紫:
zadd user:ranking:2021-03-03 Jay 3
2.過了一段時間,再獲得一個贊,可以這樣:
zincrby user:ranking:2021-03-03 Jay 1
3.如果某個用戶John作弊,需要刪除該用戶:
zrem user:ranking:2021-03-03 John
4.展示獲取贊數最多的3個用戶
zrevrangebyrank user:ranking:2021-03-03 0 2
各大網站、APP應用經常需要計數器的功能,如短視頻的播放數、電商網站的瀏覽數。這些播放數、瀏覽數一般要求實時的,每一次播放和瀏覽都要做加1的操作,如果并發量很大對于傳統關系型數據的性能是一種挑戰。Redis天然支持計數功能而且計數的性能也非常好,可以說是計數器系統的重要選擇。
如果一個分布式Web服務將用戶的Session信息保存在各自服務器,用戶刷新一次可能就需要重新登錄了,這樣顯然有問題。實際上,可以使用Redis將用戶的Session進行集中管理,每次用戶更新或者查詢登錄信息都直接從Redis中集中獲取。
幾乎每個互聯網公司中都使用了分布式部署,分布式服務下,就會遇到對同一個資源的并發訪問的技術難題,如秒殺、下單減庫存等場景。
用synchronize或者reentrantlock本地鎖肯定是不行的。
如果是并發量不大話,使用數據庫的悲觀鎖、樂觀鎖來實現沒啥問題。
但是在并發量高的場合中,利用數據庫鎖來控制資源的并發訪問,會影響數據庫的性能。
實際上,可以用Redis的setnx來實現分布式的鎖。
贊/踩、粉絲、共同好友/喜好、推送、下拉刷新等是社交網站的必備功能,由于社交網站訪問量通常比較大,而且傳統的關系型數據不太適保存 這種類型的數據,Redis提供的數據結構可以相對比較容易地實現這些功能。
消息隊列是大型網站必用中間件,如ActiveMQ、RabbitMQ、Kafka等流行的消息隊列中間件,主要用于業務解耦、流量削峰及異步處理實時性低的業務。Redis提供了發布/訂閱及阻塞隊列功能,能實現一個簡單的消息隊列系統。另外,這個不能和專業的消息中間件相比。
用于數據量上億的場景下,例如幾億用戶系統的簽到,去重登錄次數統計,某用戶是否在線狀態等等。騰訊10億用戶,要幾個毫秒內查詢到某個用戶是否在線,能怎么做?千萬別說給每個用戶建立一個key,然后挨個記(你可以算一下需要的內存會很恐怖,而且這種類似的需求很多。這里要用到位操作——使用setbit、getbit、bitcount命令。原理是:redis內構建一個足夠長的數組,每個數組元素只能是0和1兩個值,然后這個數組的下標index用來表示用戶id(必須是數字哈),那么很顯然,這個幾億長的大數組就能通過下標和元素值(0和1)來構建一個記憶系統。
Redis是基于內存的非關系型K-V數據庫,既然它是基于內存的,如果Redis服務器掛了,數據就會丟失。為了避免數據丟失了,Redis提供了持久化,即把數據保存到磁盤。
Redis提供了RDB和AOF兩種持久化機制,它持久化文件加載流程如下:
RDB,就是把內存數據以快照的形式保存到磁盤上。
什么是快照?可以這樣理解,給當前時刻的數據,拍一張照片,然后保存下來。
RDB持久化,是指在指定的時間間隔內,執行指定次數的寫操作,將內存中的數據集快照寫入磁盤中,它是Redis默認的持久化方式。執行完操作后,在指定目錄下會生成一個dump.rdb
文件,Redis 重啟的時候,通過加載dump.rdb
文件來恢復數據。RDB觸發機制主要有以下幾種:
RDB 的優點
適合大規模的數據恢復場景,如備份,全量復制等
RDB缺點
沒辦法做到實時持久化/秒級持久化。
新老版本存在RDB格式兼容問題
AOF(append only file) 持久化,采用日志的形式來記錄每個寫操作,追加到文件中,重啟時再重新執行AOF文件中的命令來恢復數據。它主要解決數據持久化的實時性問題。默認是不開啟的。
AOF的工作流程如下:
AOF的優點
數據的一致性和完整性更高
AOF的缺點
AOF記錄的內容越多,文件越大,數據恢復變慢。
我們在項目中使用Redis,肯定不會是單點部署Redis服務的。因為,單點部署一旦宕機,就不可用了。為了實現高可用,通常的做法是,將數據庫復制多個副本以部署在不同的服務器上,其中一臺掛了也可以繼續提供服務。Redis 實現高可用有三種部署模式:主從模式,哨兵模式,集群模式。
主從模式中,Redis部署了多臺機器,有主節點,負責讀寫操作,有從節點,只負責讀操作。從節點的數據來自主節點,實現原理就是主從復制機制
主從復制包括全量復制,增量復制兩種。一般當slave第一次啟動連接master,或者認為是第一次連接,就采用全量復制,全量復制流程如下:
1.slave發送sync命令到master。
2.master接收到SYNC命令后,執行bgsave命令,生成RDB全量文件。
3.master使用緩沖區,記錄RDB快照生成期間的所有寫命令。
4.master執行完bgsave后,向所有slave發送RDB快照文件。
5.slave收到RDB快照文件后,載入、解析收到的快照。
6.master使用緩沖區,記錄RDB同步期間生成的所有寫的命令。
7.master快照發送完畢后,開始向slave發送緩沖區中的寫命令;
8.salve接受命令請求,并執行來自master緩沖區的寫命令
redis2.8版本之后,已經使用psync來替代sync,因為sync命令非常消耗系統資源,psync的效率更高。
slave與master全量同步之后,master上的數據,如果再次發生更新,就會觸發增量復制。
當master節點發生數據增減時,就會觸發replicationFeedSalves()
函數,接下來在 Master節點上調用的每一個命令會使用replicationFeedSlaves()
來同步到Slave節點。執行此函數之前呢,master節點會判斷用戶執行的命令是否有數據更新,如果有數據更新的話,并且slave節點不為空,就會執行此函數。這個函數作用就是:把用戶執行的命令發送到所有的slave節點,讓slave節點執行。流程如下:
主從模式中,一旦主節點由于故障不能提供服務,需要人工將從節點晉升為主節點,同時還要通知應用方更新主節點地址。顯然,多數業務場景都不能接受這種故障處理方式。Redis從2.8開始正式提供了Redis Sentinel(哨兵)架構來解決這個問題。
哨兵模式,由一個或多個Sentinel實例組成的Sentinel系統,它可以監視所有的Redis主節點和從節點,并在被監視的主節點進入下線狀態時,自動將下線主服務器屬下的某個從節點升級為新的主節點。但是呢,一個哨兵進程對Redis節點進行監控,就可能會出現問題(單點問題),因此,可以使用多個哨兵來進行監控Redis節點,并且各個哨兵之間還會進行監控。
Sentinel哨兵模式
簡單來說,哨兵模式就三個作用:
發送命令,等待Redis服務器(包括主服務器和從服務器)返回監控其運行狀態;
哨兵監測到主節點宕機,會自動將從節點切換成主節點,然后通過發布訂閱模式通知其他的從節點,修改配置文件,讓它們切換主機;
哨兵之間還會相互監控,從而達到高可用。
故障切換的過程是怎樣的呢
假設主服務器宕機,哨兵1先檢測到這個結果,系統并不會馬上進行 failover 過程,僅僅是哨兵1主觀的認為主服務器不可用,這個現象成為主觀下線。當后面的哨兵也檢測到主服務器不可用,并且數量達到一定值時,那么哨兵之間就會進行一次投票,投票的結果由一個哨兵發起,進行 failover 操作。切換成功后,就會通過發布訂閱模式,讓各個哨兵把自己監控的從服務器實現切換主機,這個過程稱為客觀下線。這樣對于客戶端而言,一切都是透明的。
哨兵的工作模式如下:
每個Sentinel以每秒鐘一次的頻率向它所知的Master,Slave以及其他Sentinel實例發送一個 PING命令。
如果一個實例(instance)距離最后一次有效回復 PING 命令的時間超過 down-after-milliseconds 選項所指定的值, 則這個實例會被 Sentinel標記為主觀下線。
如果一個Master被標記為主觀下線,則正在監視這個Master的所有 Sentinel 要以每秒一次的頻率確認Master的確進入了主觀下線狀態。
當有足夠數量的 Sentinel(大于等于配置文件指定的值)在指定的時間范圍內確認Master的確進入了主觀下線狀態, 則Master會被標記為客觀下線。
在一般情況下, 每個 Sentinel 會以每10秒一次的頻率向它已知的所有Master,Slave發送 INFO 命令。
當Master被 Sentinel 標記為客觀下線時,Sentinel 向下線的 Master 的所有 Slave 發送 INFO 命令的頻率會從 10 秒一次改為每秒一次
若沒有足夠數量的 Sentinel同意Master已經下線, Master的客觀下線狀態就會被移除;若Master 重新向 Sentinel 的 PING 命令返回有效回復, Master 的主觀下線狀態就會被移除。
哨兵模式基于主從模式,實現讀寫分離,它還可以自動切換,系統可用性更高。但是它每個節點存儲的數據是一樣的,浪費內存,并且不好在線擴容。因此,Cluster集群應運而生,它在Redis3.0加入的,實現了Redis的分布式存儲。對數據進行分片,也就是說每臺Redis節點上存儲不同的內容,來解決在線擴容的問題。并且,它也提供復制和故障轉移的功能。
Cluster集群節點的通訊
一個Redis集群由多個節點組成,各個節點之間是怎么通信的呢?通過Gossip協議!
Redis Cluster集群通過Gossip協議進行通信,節點之前不斷交換信息,交換的信息內容包括節點出現故障、新節點加入、主從節點變更信息、slot信息等等。常用的Gossip消息分為4種,分別是:ping、pong、meet、fail。
meet消息:通知新節點加入。消息發送者通知接收者加入到當前集群,meet消息通信正常完成后,接收節點會加入到集群中并進行周期性的ping、pong消息交換。
ping消息:集群內交換最頻繁的消息,集群內每個節點每秒向多個其他節點發送ping消息,用于檢測節點是否在線和交換彼此狀態信息。
pong消息:當接收到ping、meet消息時,作為響應消息回復給發送方確認消息正常通信。pong消息內部封裝了自身狀態數據。節點也可以向集群內廣播自身的pong消息來通知整個集群對自身狀態進行更新。
fail消息:當節點判定集群內另一個節點下線時,會向集群內廣播一個fail消息,其他節點接收到fail消息之后把對應節點更新為下線狀態。
特別的,每個節點是通過集群總線(cluster bus) 與其他的節點進行通信的。通訊時,使用特殊的端口號,即對外服務端口號加10000。例如如果某個node的端口號是6379,那么它與其它nodes通信的端口號是 16379。nodes 之間的通信采用特殊的二進制協議。
Hash Slot插槽算法
既然是分布式存儲,Cluster集群使用的分布式算法是一致性Hash嘛?并不是,而是Hash Slot插槽算法。
插槽算法把整個數據庫被分為16384個slot(槽),每個進入Redis的鍵值對,根據key進行散列,分配到這16384插槽中的一個。使用的哈希映射也比較簡單,用CRC16算法計算出一個16 位的值,再對16384取模。數據庫中的每個鍵都屬于這16384個槽的其中一個,集群中的每個節點都可以處理這16384個槽。
集群中的每個節點負責一部分的hash槽,比如當前集群有A、B、C個節點,每個節點上的哈希槽數 =16384/3,那么就有:
節點A負責0~5460號哈希槽
節點B負責5461~10922號哈希槽
節點C負責10923~16383號哈希槽
Redis Cluster集群
Redis Cluster集群中,需要確保16384個槽對應的node都正常工作,如果某個node出現故障,它負責的slot也會失效,整個集群將不能工作。
因此為了保證高可用,Cluster集群引入了主從復制,一個主節點對應一個或者多個從節點。當其它主節點 ping 一個主節點 A 時,如果半數以上的主節點與 A 通信超時,那么認為主節點 A 宕機了。如果主節點宕機時,就會啟用從節點。
在Redis的每一個節點上,都有兩個玩意,一個是插槽(slot),它的取值范圍是0~16383。另外一個是cluster,可以理解為一個集群管理的插件。當我們存取的key到達時,Redis 會根據CRC16算法得出一個16 bit的值,然后把結果對16384取模。醬紫每個key都會對應一個編號在 0~16383 之間的哈希槽,通過這個值,去找到對應的插槽所對應的節點,然后直接自動跳轉到這個對應的節點上進行存取操作。
雖然數據是分開存儲在不同節點上的,但是對客戶端來說,整個集群Cluster,被看做一個整體。客戶端端連接任意一個node,看起來跟操作單實例的Redis一樣。當客戶端操作的key沒有被分配到正確的node節點時,Redis會返回轉向指令,最后指向正確的node,這就有點像瀏覽器頁面的302 重定向跳轉。
故障轉移
Redis集群實現了高可用,當集群內節點出現故障時,通過故障轉移,以保證集群正常對外提供服務。
redis集群通過ping/pong消息,實現故障發現。這個環境包括主觀下線和客觀下線。
主觀下線: 某個節點認為另一個節點不可用,即下線狀態,這個狀態并不是最終的故障判定,只能代表一個節點的意見,可能存在誤判情況。
主觀下線
客觀下線: 指標記一個節點真正的下線,集群內多個節點都認為該節點不可用,從而達成共識的結果。如果是持有槽的主節點故障,需要為該節點進行故障轉移。
假如節點A標記節點B為主觀下線,一段時間后,節點A通過消息把節點B的狀態發到其它節點,當節點C接受到消息并解析出消息體時,如果發現節點B的pfail狀態時,會觸發客觀下線流程;
當下線為主節點時,此時Redis Cluster集群為統計持有槽的主節點投票,看投票數是否達到一半,當下線報告統計數大于一半時,被標記為客觀下線狀態。
流程如下:
客觀下線
故障恢復:故障發現后,如果下線節點的是主節點,則需要在它的從節點中選一個替換它,以保證集群的高可用。流程如下:
資格檢查:檢查從節點是否具備替換故障主節點的條件。
準備選舉時間:資格檢查通過后,更新觸發故障選舉時間。
發起選舉:到了故障選舉時間,進行選舉。
選舉投票:只有持有槽的主節點才有票,從節點收集到足夠的選票(大于一半),觸發替換主節點操作
分布式鎖,是控制分布式系統不同進程共同訪問共享資源的一種鎖的實現。秒殺下單、搶紅包等等業務場景,都需要用到分布式鎖,我們項目中經常使用Redis作為分布式鎖。
選了Redis分布式鎖的幾種實現方法,大家來討論下,看有沒有啥問題哈。
命令setnx + expire分開寫
setnx + value值是過期時間
set的擴展命令(set ex px nx)
set ex px nx + 校驗唯一隨機值,再刪除
if(jedis.setnx(key,lock_value) == 1){ //加鎖
expire(key,100); //設置過期時間
try {
do something //業務請求
}catch(){
}
finally {
jedis.del(key); //釋放鎖
}
}
如果執行完setnx
加鎖,正要執行expire設置過期時間時,進程crash掉或者要重啟維護了,那這個鎖就“長生不老”了,別的線程永遠獲取不到鎖啦,所以分布式鎖不能這么實現。
long expires = System.currentTimeMillis() + expireTime; //系統時間+設置的過期時間
String expiresStr = String.valueOf(expires);
// 如果當前鎖不存在,返回加鎖成功
if (jedis.setnx(key, expiresStr) == 1) {
return true;
}
// 如果鎖已經存在,獲取鎖的過期時間
String currentValueStr = jedis.get(key);
// 如果獲取到的過期時間,小于系統當前時間,表示已經過期
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
// 鎖已過期,獲取上一個鎖的過期時間,并設置現在鎖的過期時間(不了解redis的getSet命令的小伙伴,可以去官網看下哈)
String oldValueStr = jedis.getSet(key_resource_id, expiresStr);
if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
// 考慮多線程并發的情況,只有一個線程的設置值和當前值相同,它才可以加鎖
return true;
}
}
//其他情況,均返回加鎖失敗
return false;
}
筆者看過有開發小伙伴是這么實現分布式鎖的,但是這種方案也有這些缺點:
過期時間是客戶端自己生成的,分布式環境下,每個客戶端的時間必須同步。
沒有保存持有者的唯一標識,可能被別的客戶端釋放/解鎖。
鎖過期的時候,并發多個客戶端同時請求過來,都執行了jedis.getSet()
,最終只能有一個客戶端加鎖成功,但是該客戶端鎖的過期時間,可能被別的客戶端覆蓋。
10.3:set的擴展命令(set ex px nx)(注意可能存在的問題)
if(jedis.set(key, lock_value, "NX", "EX", 100s) == 1){ //加鎖
try {
do something //業務處理
}catch(){
}
finally {
jedis.del(key); //釋放鎖
}
}
這個方案可能存在這樣的問題:
鎖過期釋放了,業務還沒執行完。
鎖被別的線程誤刪。
if(jedis.set(key, uni_request_id, "NX", "EX", 100s) == 1){ //加鎖
try {
do something //業務處理
}catch(){
}
finally {
//判斷是不是當前線程加的鎖,是才釋放
if (uni_request_id.equals(jedis.get(key))) {
jedis.del(key); //釋放鎖
}
}
}
在這里,判斷當前線程加的鎖和釋放鎖是不是一個原子操作。如果調用jedis.del()釋放鎖的時候,可能這把鎖已經不屬于當前客戶端,會解除他人加的鎖。
一般也是用lua腳本代替。lua腳本如下:
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
else
return 0
end;
這種方式比較不錯了,一般情況下,已經可以使用這種實現方式。但是存在鎖過期釋放了,業務還沒執行完的問題(實際上,估算個業務處理的時間,一般沒啥問題了)。
分布式鎖可能存在鎖過期釋放,業務沒執行完的問題。有些小伙伴認為,稍微把鎖過期時間設置長一些就可以啦。其實我們設想一下,是否可以給獲得鎖的線程,開啟一個定時守護線程,每隔一段時間檢查鎖是否還存在,存在則對鎖的過期時間延長,防止鎖過期提前釋放。
當前開源框架Redisson就解決了這個分布式鎖問題。我們一起來看下Redisson底層原理是怎樣的吧:
只要線程一加鎖成功,就會啟動一個watch dog
看門狗,它是一個后臺線程,會每隔10秒檢查一下,如果線程1還持有鎖,那么就會不斷的延長鎖key的生存時間。因此,Redisson就是使用Redisson解決了鎖過期釋放,業務沒執行完問題。
Redis一般都是集群部署的,假設數據在主從同步過程,主節點掛了,Redis分布式鎖可能會有哪些問題呢?一起來看些這個流程圖:
如果線程一在Redis的master節點上拿到了鎖,但是加鎖的key還沒同步到slave節點。恰好這時,master節點發生故障,一個slave節點就會升級為master節點。線程二就可以獲取同個key的鎖啦,但線程一也已經拿到鎖了,鎖的安全性就沒了。
為了解決這個問題,Redis作者 antirez提出一種高級的分布式鎖算法:Redlock。Redlock核心思想是這樣的:
搞多個Redis master部署,以保證它們不會同時宕掉。并且這些master節點是完全相互獨立的,相互之間不存在數據同步。同時,需要確保在這多個master實例上,是與在Redis單實例,使用相同方法來獲取和釋放鎖。
我們假設當前有5個Redis master節點,在5臺服務器上面運行這些Redis實例。
RedLock的實現步驟:如下
1.獲取當前時間,以毫秒為單位。
2.按順序向5個master節點請求加鎖。客戶端設置網絡連接和響應超時時間,并且超時時間要小于鎖的失效時間。(假設鎖自動失效時間為10秒,則超時時間一般在5-50毫秒之間,我們就假設超時時間是50ms吧)。如果超時,跳過該master節點,盡快去嘗試下一個master節點。
3.客戶端使用當前時間減去開始獲取鎖時間(即步驟1記錄的時間),得到獲取鎖使用的時間。當且僅當超過一半(N/2+1,這里是5/2+1=3個節點)的Redis master節點都獲得鎖,并且使用的時間小于鎖失效時間時,鎖才算獲取成功。(如上圖,10s> 30ms+40ms+50ms+4m0s+50ms)
如果取到了鎖,key的真正有效時間就變啦,需要減去獲取鎖所使用的時間。
如果獲取鎖失敗(沒有在至少N/2+1個master實例取到鎖,有或者獲取鎖時間已經超過了有效時間),客戶端要在所有的master節點上解鎖(即便有些master節點根本就沒有加鎖成功,也需要解鎖,以防止有些漏網之魚)。
簡化下步驟就是:
按順序向5個master節點請求加鎖
根據設置的超時時間來判斷,是不是要跳過該master節點。
如果大于等于三個節點加鎖成功,并且使用的時間小于鎖的有效期,即可認定加鎖成功啦。
如果獲取鎖失敗,解鎖!
跳躍表
跳躍表是有序集合zset的底層實現之一
跳躍表支持平均O(logN),最壞 O(N)復雜度的節點查找,還可以通過順序性操作批量處理節點。
跳躍表實現由zskiplist和zskiplistNode兩個結構組成,其中zskiplist用于保存跳躍表信息(如表頭節點、表尾節點、長度),而zskiplistNode則用于表示跳躍表節點。
跳躍表就是在鏈表的基礎上,增加多級索引提升查找效率。
緩存延時雙刪
刪除緩存重試機制
讀取biglog異步刪除緩存
什么是延時雙刪呢?流程圖如下:
延時雙刪流程
先刪除緩存
再更新數據庫
休眠一會(比如1秒),再次刪除緩存。
這個休眠一會,一般多久呢?都是1秒?
這個休眠時間 = 讀業務邏輯數據的耗時 + 幾百毫秒。為了確保讀請求結束,寫請求可以刪除讀請求可能帶來的緩存臟數據。
這種方案還算可以,只有休眠那一會(比如就那1秒),可能有臟數據,一般業務也會接受的。但是如果第二次刪除緩存失敗呢?緩存和數據庫的數據還是可能不一致,對吧?給Key設置一個自然的expire過期時間,讓它自動過期怎樣?那業務要接受過期時間內,數據的不一致咯?還是有其他更佳方案呢?
因為延時雙刪可能會存在第二步的刪除緩存失敗,導致的數據不一致問題。可以使用這個方案優化:刪除失敗就多刪除幾次呀,保證刪除緩存成功就可以了呀~ 所以可以引入刪除緩存重試機制
刪除緩存重試流程
寫請求更新數據庫
緩存因為某些原因,刪除失敗
把刪除失敗的key放到消息隊列
消費消息隊列的消息,獲取要刪除的key
重試刪除緩存操作
重試刪除緩存機制還可以吧,就是會造成好多業務代碼入侵。其實,還可以這樣優化:通過數據庫的binlog來異步淘汰key。
以mysql為例吧
可以使用阿里的canal將binlog日志采集發送到MQ隊列里面
然后通過ACK機制確認處理這條更新消息,刪除緩存,保證數據緩存一致性
Redis6.0之前,Redis在處理客戶端的請求時,包括讀socket、解析、執行、寫socket等都由一個順序串行的主線程處理,這就是所謂的“單線程”。
Redis6.0之前為什么一直不使用多線程?使用Redis時,幾乎不存在CPU成為瓶頸的情況, Redis主要受限于內存和網絡。例如在一個普通的Linux系統上,Redis通過使用pipelining每秒可以處理100萬個請求,所以如果應用程序主要使用O(N)或O(log(N))的命令,它幾乎不會占用太多CPU。
redis使用多線程并非是完全摒棄單線程,redis還是使用單線程模型來處理客戶端的請求,只是使用多線程來處理數據的讀寫和協議解析,執行命令還是使用單線程。
這樣做的目的是因為redis的性能瓶頸在于網絡IO而非CPU,使用多線程能提升IO讀寫的效率,從而整體提高redis的性能。
Redis通過MULTI、EXEC、WATCH等一組命令集合,來實現事務機制。事務支持一次執行多個命令,一個事務中所有命令都會被序列化。在事務執行過程,會按照順序串行化執行隊列中的命令,其他客戶端提交的命令請求不會插入到事務執行命令序列中。
簡言之,Redis事務就是順序性、一次性、排他性的執行一個隊列中的一系列命令。
Redis執行事務的流程如下:
開始事務(MULTI)
命令入隊
執行事務(EXEC)、撤銷事務(DISCARD )
Redis 作為一個K-V的內存數據庫,它使用用一張全局的哈希來保存所有的鍵值對。這張哈希表,有多個哈希桶組成,哈希桶中的entry元素保存了key和value指針,其中*key指向了實際的鍵,*value指向了實際的值。
哈希表查找速率很快的,有點類似于Java中的HashMap,它讓我們在O(1) 的時間復雜度快速找到鍵值對。首先通過key計算哈希值,找到對應的哈希桶位置,然后定位到entry,在entry找到對應的數據。
什么是哈希沖突?
哈希沖突:通過不同的key,計算出一樣的哈希值,導致落在同一個哈希桶中。
Redis為了解決哈希沖突,采用了鏈式哈希。鏈式哈希是指同一個哈希桶中,多個元素用一個鏈表來保存,它們之間依次用指針連接。
有些讀者可能還會有疑問:哈希沖突鏈上的元素只能通過指針逐一查找再操作。當往哈希表插入數據很多,沖突也會越多,沖突鏈表就會越長,那查詢效率就會降低了。
為了保持高效,Redis 會對哈希表做rehash操作,也就是增加哈希桶,減少沖突。為了rehash更高效,Redis還默認使用了兩個全局哈希表,一個用于當前使用,稱為主哈希表,一個用于擴容,稱為備用哈希表。
可以的,Redis提供兩個指令生成RDB,分別是save和bgsave。
如果是save指令,會阻塞,因為是主線程執行的。
如果是bgsave指令,是fork一個子進程來寫入RDB文件的,快照持久化完全交給子進程來處理,父進程則可以繼續處理客戶端的請求。
RESP,英文全稱是Redis Serialization Protocol,它是專門為redis設計的一套序列化協議. 這個協議其實在redis的1.2版本時就已經出現了,但是到了redis2.0才最終成為redis通訊協議的標準。
RESP主要有實現簡單、解析速度快、可讀性好等優點。
應對緩存穿透問題,我們可以使用布隆過濾器。布隆過濾器是什么呢?
布隆過濾器是一種占用空間很小的數據結構,它由一個很長的二進制向量和一組Hash映射函數組成,它用于檢索一個元素是否在一個集合中,空間效率和查詢時間都比一般的算法要好的多,缺點是有一定的誤識別率和刪除困難。
布隆過濾器原理是?假設我們有個集合A,A中有n個元素。利用k個哈希散列函數,將A中的每個元素映射到一個長度為a位的數組B中的不同位置上,這些位置上的二進制數均設置為1。如果待檢查的元素,經過這k個哈希散列函數的映射后,發現其k個位置上的二進制數全部為1,這個元素很可能屬于集合A,反之,一定不屬于集合A。
來看個簡單例子吧,假設集合A有3個元素,分別為{d1,d2,d3}。有1個哈希函數,為Hash2。現在將A的每個元素映射到長度為16位數組B。
我們現在把d1映射過來,假設Hash2(d1)= 2,我們就把數組B中,下標為2的格子改成1,如下:
我們現在把d2也映射過來,假設Hash2(d2)= 5,我們把數組B中,下標為5的格子也改成1,如下:
接著我們把d3也映射過來,假設Hash2(d3)也等于 2,它也是把下標為2的格子標1:
因此,我們要確認一個元素dn是否在集合A里,我們只要算出Hash2(dn)得到的索引下標,只要是0,那就表示這個元素不在集合A,如果索引下標是1呢?那該元素可能是A中的某一個元素。因為你看,d1和d3得到的下標值,都可能是1,還可能是其他別的數映射的,布隆過濾器是存在這個缺點的:會存在hash碰撞導致的假陽性,判斷存在誤差。
如何減少這種誤差呢?
搞多幾個哈希函數映射,降低哈希碰撞的概率
同時增加B數組的bit長度,可以增大hash函數生成的數據的范圍,也可以降低哈希碰撞的概率
我們又增加一個Hash3哈希映射函數,假設Hash3(d1)=6,Hash3(d3)=8,它倆不就不沖突了嘛,如下:
即使存在誤差,我們可以發現,布隆過濾器并沒有存放完整的數據,它只是運用一系列哈希映射函數計算出位置,然后填充二進制向量。如果數量很大的話,布隆過濾器通過極少的錯誤率,換取了存儲空間的極大節省,還是挺劃算的。
目前布隆過濾器已經有相應實現的開源類庫啦,如Google的Guava類庫,Twitter的 Algebird 類庫,信手拈來即可,或者基于Redis自帶的Bitmaps自行實現設計也是可以的。
感謝各位的閱讀,以上就是“Redis經典面試題及答案有哪些”的內容了,經過本文的學習后,相信大家對Redis經典面試題及答案有哪些這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。