首頁  >  文章  >  資料庫  >  20個Redis經典面試題彙整及答案(分享)

20個Redis經典面試題彙整及答案(分享)

青灯夜游
青灯夜游轉載
2023-03-07 18:53:414631瀏覽

這篇文章幫大家整理了20道經典Redis面試題,希望對大家有幫助。

20個Redis經典面試題彙整及答案(分享)

1. 什麼是Redis?它主要用來什麼的?

Redis,英文全名是Remote Dictionary Server(遠端字典服務),是一個開源的使用ANSI C語言編寫、支援網路、可基於記憶體亦可持久化的日誌型、 Key-Value資料庫,並提供多種語言的API。

與MySQL資料庫不同的是,Redis的資料是存在記憶體中的。它的讀寫速度非常快,每秒鐘可以處理超過10萬次讀寫操作。因此redis被廣泛應用於快取,另外,Redis也常用來做分散式鎖定。除此之外,Redis支援交易、持久化、LUA 腳本、LRU 驅動事件、多種叢集方案。

2.說說Redis的基本資料結構類型

大多數小夥伴都知道,Redis有以下這五種基本類型:

  • String(字符串)
  • Hash(雜湊)
  • List(列表)
  • Set(集合)
  • zset(有序集合)

它也有三種特殊的資料結構類型

  • Geospatial
  • #Hyperloglog
  • Bitmap

#2.1 Redis 的五種基本資料類型

String(字串)

    ##簡介:String是Redis最基礎的資料結構類型,它是二進位安全的,可以儲存圖片或序列化的對象,值最大儲存為512M
  • #簡單使用範例: 
  • set key valueget 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(跳躍表)
  • 應用場景:排行榜,社交需求(如用戶按讚)。

2.2 Redis 的三種特殊資料類型

  • #Geo:Redis3.2推出的,地理位置定位,用於儲存地理位置信息,並對儲存的資訊進行操作。
  • HyperLogLog:用來做基數統計演算法的資料結構,如統計網站的UV。
  • Bitmaps :用一個位元位元來映射某個元素的狀態,在Redis中,它的底層是基於字串類型實現的,可以把bitmaps成作一個以位元為單位的陣列

3. Redis為什麼這麼快?

Redis為什麼這麼快

3.1 基於記憶體儲存實作

我們都知道記憶體讀寫是比在磁碟快很多的,Redis基於記憶體儲存實作的資料庫,相對於資料存在磁碟的MySQL資料庫,省去磁碟I/O的消耗。

3.2 高效的資料結構

我們知道,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)複雜度的節點查找,還可以透過順序性操作批次處理節點。

3.3 合理的資料編碼

#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(跳躍表)編碼#​​

3.4 合理的執行緒模型

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 引入了多執行緒提速,它的執行指令操作記憶體的仍然是個單執行緒。

3.5 虛擬記憶體機制

Redis直接自己建構了VM機制,不會像一般的系統會呼叫系統函數處理,會浪費一定的時間去移動和請求。

Redis的虛擬記憶體機制是啥呢?

虛擬記憶體機制就是暫時把不經常存取的資料(冷資料)從記憶體交換到磁碟中,從而騰出寶貴的記憶體空間用於其它需要存取的資料(熱數據)。透過VM功能可以實現冷熱資料分離,使熱資料仍在記憶體中、冷資料儲存到磁碟。這樣就可以避免因為記憶體不足而造成存取速度下降的問題。

4. 什麼是快取擊穿、快取穿透、快取雪崩?

4.1 快取穿透問題

先來看一個常見的快取使用方式:讀取請求來了,先查下緩存,快取有值命中,就直接返回;快取沒命中,就去查資料庫,然後把資料庫的值更新到緩存,再返回。

讀取快取

快取穿透:指查詢一個一定不存在的數據,由於快取是不命中時需要從資料庫查詢,查不到資料則不寫入緩存,這將導致這個不存在的資料每次請求都要到資料庫去查詢,進而給資料庫帶來壓力。

通俗點說,讀取請求存取時,快取和資料庫都沒有某個值,這樣就會導致每次對這個值的查詢請求都會穿透到資料庫,這就是快取穿透。

快取穿透一般都是這幾種情況產生的:

  • #業務不合理的設計,例如大多數用戶都沒開守護,但是你的每個請求都去緩存,查詢某個userid查詢有沒有守護。
  • 業務/維運/開發錯誤的操作,例如快取和資料庫的資料都被誤刪除了。
  • 駭客非法要求攻擊,例如駭客故意捏造大量非法請求,以讀取不存在的業務資料。

如何避免快取穿透呢?  一般有三種方法。

  • 1.如果是非法請求,我們在API入口,對參數進行校驗,過濾非法值。
  • 2.如果查詢資料庫為空,我們可以為快取設定個空值,或是預設值。但是如有寫請求進來的話,需要更新快取哈,以確保快取一致性,同時,最後給快取設定適當的過期時間。 (業務上比較常用,簡單有效)
  • 3.使用布隆過濾器快速判斷資料是否存在。即一個查詢請求過來時,先透過布隆過濾器判斷值是否存在,存在才繼續往下查。

布隆過濾器原理:它由初始值為0的位圖數組和N個雜湊函數組成。一個對一個key進行N個hash演算法取得N個值,在位元數組中將這N個值散列後設定為1,然後查的時候如果特定的這幾個位置都為1,那麼布隆過濾器判斷該key存在。

4.2 快取雪奔問題

#快取雪奔: 指快取中資料大批量到過期時間,而查詢資料量巨大,請求都直接存取資料庫,造成資料庫壓力過大甚至down機。

  • 快取雪奔一般是由於大量資料同時過期造成的,對於這個原因,可透過均勻設定過期時間解決,即讓過期時間相對離散一點。如採用一個較大固定值 一個較小的隨機值,5小時 0到1800秒醬紫。
  • Redis 故障宕機也可能造成快取雪奔。這就需要構造Redis高可用群集啦。

4.3 快取擊穿問題

#快取擊穿: 指熱點key在某個時間點過期的時候,而剛好在這個時間點對這個Key有大量的並發請求過來,從而大量的請求打到db。

快取擊穿看著有點像,其實它兩區別是,快取雪奔是指資料庫壓力過大甚至down機,快取擊穿只是大量並發請求到了DB資料庫層面。可以認為擊穿是緩存雪奔的子集吧。有些文章認為它倆區別,是區別在於擊穿針對某一熱點key緩存,雪奔則是很多key。

解決方案有兩種:

  • 1.使用互斥鎖定方案。快取失效時,不是立即去載入db數據,而是先使用某些帶成功返回的原子操作指令,如(Redis的setnx)去操作,成功的時候,再去載入db資料庫資料和設定快取。否則就去重試取得快取。
  • 2. “永不過期”,是指沒有設定過期時間,但是熱點資料快要過期時,非同步執行緒去更新和設定過期時間。

5. 什麼是熱Key問題,如何解決熱key問題

什麼是熱Key呢?在Redis中,我們把訪問頻率高的key,稱為熱點key。

如果某一熱點key的請求到伺服器主機時,由於請求量特別大,可能會導致主機資源不足,甚至宕機,從而影響正常的服務。

而熱點Key又是怎麼產生的呢?主要原因有兩個:

  • 用戶消費的數據遠大於生產的數據,如秒殺、熱點新聞等讀取多寫少的場景。
  • 請求分片集中,超過單一Redi伺服器的效能,例如固定名稱key,Hash落入同一台伺服器,瞬​​間訪問量極大,超過機器瓶頸,產生熱點Key問題。

那麼在日常開發中,如何辨識到熱點key呢?

  • 以經驗判斷哪些是熱Key;
  • 客戶端統計上報;
  • 服務代理程式層上報

如何解決熱key問題?

  • Redis叢集擴容:增加分片副本,均衡讀取流量;
  • 將熱key分散到不同的伺服器;
  • 使用二級緩存,即JVM本地緩存,減少Redis的讀取請求。

6. Redis 過期策略與記憶體淘汰策略

6.1 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种内存淘汰策略保护自己~

6.2 Redis 内存淘汰策略

  • 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:默认策略,当内存不足以容纳新写入数据时,新写入操作会报错。

7.说说Redis的常用应用场景

  • 缓存
  • 排行榜
  • 计数器应用
  • 共享Session
  • 分布式锁
  • 社交网络
  • 消息队列
  • 位操作

7.1 缓存

我们一提到redis,自然而然就想到缓存,国内外中大型的网站都离不开缓存。合理的利用缓存,比如缓存热点数据,不仅可以提升网站的访问速度,还可以降低数据库DB的压力。并且,Redis相比于memcached,还提供了丰富的数据结构,并且提供RDB和AOF等持久化机制,强的一批。

7.2 排行榜

当今互联网应用,有各种各样的排行榜,如电商网站的月度销量排行榜、社交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

7.3 计数器应用

各大网站、APP应用经常需要计数器的功能,如短视频的播放数、电商网站的浏览数。这些播放数、浏览数一般要求实时的,每一次播放和浏览都要做加1的操作,如果并发量很大对于传统关系型数据的性能是一种挑战。Redis天然支持计数功能而且计数的性能也非常好,可以说是计数器系统的重要选择。

7.4 共享Session

如果一个分布式Web服务将用户的Session信息保存在各自服务器,用户刷新一次可能就需要重新登录了,这样显然有问题。实际上,可以使用Redis将用户的Session进行集中管理,每次用户更新或者查询登录信息都直接从Redis中集中获取。

7.5 分布式锁

几乎每个互联网公司中都使用了分布式部署,分布式服务下,就会遇到对同一个资源的并发访问的技术难题,如秒杀、下单减库存等场景。

  • 用synchronize或reentrantlock本地鎖肯定是不行的。
  • 如果是並發量不大話,使用資料庫的悲觀鎖、樂觀鎖來實現沒啥問題。
  • 但是在並發量高的場合中,利用資料庫鎖定來控制資源的並發訪問,會影響資料庫的效能。
  • 實際上,可以用Redis的setnx來實現分散式的鎖定。

7.6 社群網路

讚/踩、粉絲、共同好友/喜好、推播、下拉刷新等是社群網站的必備功能,由於社交網站訪問量通常比較大,而且傳統的關係型數據不太適保存這種類型的數據,Redis提供的數據結構可以相對比較容易地實現這些功能。

7.7 訊息佇列

訊息佇列是大型網站必用中間件,如ActiveMQ、RabbitMQ、Kafka等流行的訊息佇列中介軟體,主要用於業務解耦、流量削峰及非同步處理即時性低的業務。 Redis提供了發布/訂閱及阻塞佇列功能,能實現一個簡單的訊息佇列系統。另外,這個不能和專業的訊息中間件相比。

7.8 位元操作

用於資料量上億的場景下,例如數億使用者係統的簽到,去重登入次數統計,某使用者是否線上狀態等等。騰訊10億用戶,要幾個毫秒內查詢到某個用戶是否在線,能怎麼做?千萬別說給每個使用者建立一個key,然後挨個記(你可以算一下需要的記憶體會很恐怖,而且這種類似的需求很多。這裡要用到位操作-使用setbit、getbit、bitcount指令。原理是:redis內建構一個足夠長的數組,每個數組元素只能是0和1兩個值,然後這個數組的下標index用來表示用戶id(必須是數字哈),那麼很顯然,這個幾億長的大數組就能透過下標和元素值(0和1)來建構一個記憶系統。

8. Redis 的持久化機制有哪些?優缺點說說

Redis是基於記憶體的非關係型K-V資料庫,既然它是基於記憶體的,如果Redis伺服器掛了,資料就會遺失。為了避免資料遺失了,Redis提供了持久化,也就是把資料存到磁碟。

Redis提供了RDB和AOF兩種持久化機制,它持久化檔案載入流程如下:

8.1 RDB

RDB,就是以快照的形式儲存記憶體資料。

什麼是快照?可以這樣理解,給當前時刻的數據,拍一張照片,然後保存下來。

RDB持久化,是指在指定的時間間隔內,執行指定次數的寫入操作,將記憶體中的資料集快照寫入磁碟中,它是Redis預設的持久化方式。執行完操作後,在指定目錄下會產生一個dump.rdb文件,Redis 重新啟動的時候,透過載入dump.rdb檔案來恢復資料。RDB觸發機制主要有以下幾種:

##RDB 的優點

    適合大規模的資料復原場景,如備份,全量複製等

RDB缺點

    沒辦法做到即時持久化/秒級持久化。
  • 新舊版存在RDB格式相容問題

AOF

AOF(append only file) 持久化,採用日誌的形式來記錄每個寫入操作,追加到檔案中,重新啟動時重新執行AOF檔案中的命令來復原資料。它主要解決資料持久化的即時性問題。預設是不開啟的。

AOF的工作流程如下:

AOF的優點

    資料的一致性和完整性較高

AOF的缺點

    AOF記錄的內容越多,文件越大,資料恢復變慢。

9.怎麼實現Redis的高可用?

我們在專案中使用Redis,絕對不會是單點部署Redis服務的。因為,單點部署一旦宕機,就不可用了。為了實現高可用,通常的做法是,將資料庫複製多個副本以部署在不同的伺服器上,其中一台掛了也可以繼續提供服務。 Redis 實作高可用有三種部署模式:

主從模式,哨兵模式,叢集模式

9.1 主從模式

主從模式中,Redis部署了多台機器,有主節點,負責讀寫操作,有從節點,只負責讀操作。從節點的資料來自主節點,實作原理就是

主從複製機制#

主從複製包括全量複製,增量複製兩種。一般當slave第一次啟動連接master,或認為第一次連接,就採用全量複製,全量複製流程如下:

  • 1.slave發送sync指令到master。
  • 2.master接收到SYNC指令後,執行bgsave指令,產生RDB全量檔。
  • 3.master使用緩衝區,記錄RDB快照產生期間的所有寫入指令。
  • 4.master執行完bgsave後,傳送RDB快照檔給所有slave。
  • 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節點執行。流程如下:

9.2 哨兵模式

主從模式中,一旦主節點因故障而無法提供服務,需要人工將從節點晉升為主節點,同時也要通知應用程式更新主節點位址。顯然,多數業務場景都不能接受這種故障處理方式。 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 的主觀下線狀態就會被移除。

9.3 Cluster叢集模式

#哨兵模式基於主從模式,實現讀寫分離,它還可以自動切換,系統可用性更高。但是它每個節點存儲的數據是一樣的,浪費內存,並且不好在線擴容。因此,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集群为统计持有槽的主节点投票,看投票数是否达到一半,当下线报告统计数大于一半时,被标记为客观下线状态。

流程如下:

客观下线

故障恢复:故障发现后,如果下线节点的是主节点,则需要在它的从节点中选一个替换它,以保证集群的高可用。流程如下:

  • 资格检查:检查从节点是否具备替换故障主节点的条件。
  • 准备选举时间:资格检查通过后,更新触发故障选举时间。
  • 发起选举:到了故障选举时间,进行选举。
  • 选举投票:只有持有槽的主节点才有票,从节点收集到足够的选票(大于一半),触发替换主节点操作

10. 使用过Redis分布式锁嘛?有哪些注意点呢?

分布式锁,是控制分布式系统不同进程共同访问共享资源的一种锁的实现。秒杀下单、抢红包等等业务场景,都需要用到分布式锁,我们项目中经常使用Redis作为分布式锁。

选了Redis分布式锁的几种实现方法,大家来讨论下,看有没有啥问题哈。

  • 命令setnx + expire分开写
  • setnx + value值是过期时间
  • set的扩展命令(set ex px nx)
  • set ex px nx + 校验唯一随机值,再删除

10.1 命令setnx + expire分开写

if(jedis.setnx(key,lock_value) == 1){ //加锁
    expire(key,100); //设置过期时间
    try {
        do something  //业务请求
    }catch(){
  }
  finally {
       jedis.del(key); //释放锁
    }
}

如果执行完setnx加锁,正要执行expire设置过期时间时,进程crash掉或者要重启维护了,那这个锁就“长生不老”了,别的线程永远获取不到锁啦,所以分布式锁不能这么实现。

10.2 setnx + value值是过期时间

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); //释放锁
    }
}

这个方案可能存在这样的问题:

  • 锁过期释放了,业务还没执行完。
  • 锁被别的线程误删。

10.4 set ex px nx + 校验唯一随机值,再删除

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(&#39;get&#39;,KEYS[1]) == ARGV[1] then 
   return redis.call(&#39;del&#39;,KEYS[1]) 
else
   return 0
end;

这种方式比较不错了,一般情况下,已经可以使用这种实现方式。但是存在锁过期释放了,业务还没执行完的问题(实际上,估算个业务处理的时间,一般没啥问题了)。

11. 使用过Redisson嘛?说说它的原理

分布式锁可能存在锁过期释放,业务没执行完的问题。有些小伙伴认为,稍微把锁过期时间设置长一些就可以啦。其实我们设想一下,是否可以给获得锁的线程,开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放。

当前开源框架Redisson就解决了这个分布式锁问题。我们一起来看下Redisson底层原理是怎样的吧:

只要线程一加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程1还持有锁,那么就会不断的延长锁key的生存时间。因此,Redisson就是使用Redisson解决了锁过期释放,业务没执行完问题。

12. 什么是Redlock算法

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节点。
  • 如果大于等于三个节点加锁成功,并且使用的时间小于锁的有效期,即可认定加锁成功啦。
  • 如果获取锁失败,解锁!

13. Redis的跳跃表

跳跃表

  • 跳躍表是有序集合zset的底層實作之一
  • 跳躍表支援平均O(logN),最壞O(N)複雜度的節點查找,也可以透過順序性操作批次處理節點。
  • 跳躍表實作由zskiplist和zskiplistNode兩個結構組成,其中zskiplist用於保存跳躍表資訊(如表頭節點、表尾節點、長度),而zskiplistNode則用於表示跳躍表節點。
  • 跳躍表就是在鍊錶的基礎上,增加多層索引提升查找效率。

14. MySQL與Redis 如何保證雙重寫入一致性

  • 快取延時雙刪
  • 刪除快取重試機制
  • 讀取biglog非同步刪除快取

14.1 延時雙刪?

什麼是延時雙刪呢?流程圖如下:

延時雙刪流程

  1. #先刪除快取
  2. 再更新資料庫
  3. 休眠一會(例如1秒),再次刪除快取。

這個休眠一會,通常多久呢?都是1秒?

這個休眠時間 = 讀取業務邏輯資料的耗時 幾百毫秒。為了確保讀取請求結束,寫入請求可以刪除讀取請求可能帶來的快取髒資料。

這種方案還算可以,只有休眠那一會(比如就那1秒),可能有髒數據,一般業務也會接受的。但是如果第二次刪除快取失敗呢?快取和資料庫的資料還是可能不一致,對吧?給Key設定一個自然的expire過期時間,讓它自動過期怎麼樣?那業務要接受過期時間內,數據的不一致咯?還是有其他更佳方案呢?

14.2 刪除快取重試機制

因為延時雙刪可能會存在第二步驟的刪除快取失敗,導致的資料不一致問題。可以使用這個方案優化:刪除失敗就多刪除幾次呀,保證刪除快取成功就可以了呀~ 所以可以引入刪除快取重試機制

##刪除快取重試流程

    寫入請求更新資料庫
  1. 快取因為某些原因,刪除失敗
  2. #把刪除失敗的key放到訊息佇列
  3. #消費訊息佇列的訊息,取得要刪除的key
  4. 重試刪除快取操作
#14.3 讀取biglog非同步刪除快取

重試刪除快取機制還可以吧,就是會造成好多

業務程式碼入侵。其實,還可以這樣優化:透過資料庫的binlog來非同步淘汰key。

以mysql為例吧

    可以使用阿里的canal將binlog日誌擷取傳送到MQ佇列裡面
  • 然後透過ACK機制確認處理這則更新訊息,刪除緩存,確保資料快取一致性
15. 為什麼Redis 6.0 之後改多執行緒呢?

    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的效能。

16. 聊聊Redis 交易機制

Redis透過

MULTI、EXEC、WATCH等一組指令集合,來實現交易機制。事務支援一次執行多個命令,一個事務中所有命令都會被序列化。在事務執行過程,會依照順序串列化執行佇列中的命令,其他客戶端提交的命令請求不會插入到交易執行命令序列中。

簡言之,Redis事務就是

順序性、一次性、排他性的執行一個佇列中的一系列指令。

Redis執行交易的流程如下:

    開始交易(MULTI)
  • 命令入隊
  • 執行交易(EXEC)、撤銷事務(DISCARD )
17. Redis的Hash 衝突怎麼辦

Redis 作為一個K-V的記憶體資料庫,它使用用一張全域的哈希來保存所有的鍵值對。這張哈希表,有多個哈希桶組成,哈希桶中的entry元素保存了

key和value指針,其中*key指向了實際的鍵,*value指向了實際的值。

哈希表查找速率很快的,有點類似Java中的HashMap,它讓我們在O(1) 的時間複雜度快速找到鍵值對。首先透過key計算哈希值,找到對應的哈希桶位置,然後定位到entry,在entry找到對應的資料。

什麼是哈希衝突?

哈希衝突:透過不同的key,計算相同的雜湊值,導致落在同一個哈希桶中。

Redis為了解決哈希衝突,採用了鍊式哈希。鍊式雜湊是指同一個雜湊桶中,多個元素用一個鍊錶來保存,它們之間依序用指標連接。

有些讀者可能還會有疑問:哈希衝突鏈上的元素只能透過指標逐一找到再操作。當往哈希表插入資料很多,衝突也會越多,衝突鍊錶就會越長,那麼查詢效率就會降低了。

為了保持高效,Redis 會對哈希表做rehash操作,也就是增加哈希桶,減少衝突。為了rehash更有效率,Redis也預設使用了兩個全域雜湊表,一個用於目前使用,稱為主雜湊表,一個用於擴容,稱為備用雜湊表

18. 在產生 RDB期間,Redis 可以同時處理寫入請求麼?

可以的,Redis提供兩個指令產生RDB,分別是save和bgsave

  • 如果是save指令,會阻塞,因為是主執行緒執行的。
  • 如果是bgsave指令,是fork一個子程序來寫入RDB檔案的,快照持久化完全交給子程序來處理,父程序則可以繼續處理客戶端的請求。

19. Redis底層,使用的什麼協定?

RESP,英文全名為Redis Serialization Protocol,它是專門為redis設計的一套序列化協定. 這個協定其實在redis的1.2版本時就已經出現了,但是到了redis2.0才最終成為redis通訊協議的標準。

RESP主要有實作簡單、解析速度快、可讀性好等優點。

20. 布隆過濾器

應對快取穿透問題,我們可以使用布隆過濾器。布隆過濾器是什麼呢?

布隆過濾器是一種佔用空間很小的資料結構,它由一個很長的二進位向量和一組Hash映射函數組成,它用於檢索一個元素是否在一個集合中,空間效率和查詢時間都比一般的演算法好的多,缺點是有一定的誤辨識率和刪除困難。

布隆過濾器原理是? 假設我們有個集合A,A中有n個元素。利用k個雜湊雜湊函數,將A中的每個元素映射到長度為a位的陣列B中的不同位置上,這些位置上的二進位數均設定為1。如果待檢查的元素,經過這k個雜湊函數的映射後,發現其k個位置上的二進制數全部為1,這個元素很可能屬於集合A,反之,一定不屬於集合A

來看個簡單例子吧,假設集合A有3個元素,分別為{d1,d2,d3}。有1個哈希函數,為Hash1。現在將A的每個元素映射到長度為16位數組B。

我們現在把d1映射過來,假設Hash1(d1)= 2,我們就把數組B中,下標為2的格子改成1,如下:

我們現在把d2也映射過來,假設Hash1(d2)= 5,我們把陣列B中,下標為5的格子也改成1,如下:

接著我們把d3也映射過來,假設Hash1(d3)也等於2,它也是把下標為2的格子標1:

因此,我們要確認一個元素dn是否在集合A裡,我們只要算出Hash1(dn)得到的索引下標,只要是0 ,那就表示這個元素不在集合A,如果索引下標是1呢?那該元素可能是A中的某一個元素。因為你看,d1和d3得到的下標值,都可能是1,還可能是其他別的數映射的,布隆過濾器是存在這個缺點的:會存在hash碰撞導致的假陽性,判斷存在誤差。

如何減少這個誤差呢?

  • 搞多幾個雜湊函數映射,降低雜湊碰撞的機率
  • 同時增加B數組的bit長度,可以增加hash函數產生的資料的範圍,也可以降低哈希碰撞的機率

我們又增加一個Hash2哈希映射函數,假設Hash2(d1)=6,Hash2(d3)=8,它兩個不就不衝突了嘛,如下:

即使存在誤差,我們可以發現,布隆過濾器並沒有存放完整的資料,它只是運用一系列雜湊映射函數計算出位置,然後填入二進位向量。如果數量很大的話,布隆過濾器透過極少的錯誤率,換取了儲存空間的極大節省,還是挺划算的。

目前布林過濾器已經有對應實作的開源類別庫啦,例如Google的Guava類別庫,Twitter的Algebird 類別庫,信手拈來即可,或是基於Redis自帶的Bitmaps自行實作設計也是可以的。

更多程式相關知識,請造訪:程式設計影片! !

以上是20個Redis經典面試題彙整及答案(分享)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:csdn.net。如有侵權,請聯絡admin@php.cn刪除