1. 程式人生 > 其它 >Redis奪命20問

Redis奪命20問

前言

大家好,我是撿田螺的小男孩。金九銀十即將到來,整理了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原始碼如下:

structsdshdr{
unsignedintlen;//標記buf的長度
unsignedintfree;//標記buf中未使用的元素個數
charbuf[];//存放元素的坑
}

SDS 結構圖如下:

Redis為什麼選擇SDS結構,而C語言原生的char[]不香嗎?

舉例其中一點,SDS中,O(1)時間複雜度,就可以獲取字串長度;而C 字串,需要遍歷整個字串,時間複雜度為O(n)

Hash(雜湊)

  • 簡介:在Redis中,雜湊型別是指v(值)本身又是一個鍵值對(k-v)結構
  • 簡單使用舉例:hset key field valuehget 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為什麼這麼快?

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複用技術可以讓單個執行緒高效的處理多個連線請求,而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個贊,可以醬紫:
zadduser:ranking:2021-03-03Jay3
  • 2.過了一段時間,再獲得一個贊,可以這樣:
zincrbyuser:ranking:2021-03-03Jay1
  • 3.如果某個使用者John作弊,需要刪除該使用者:
zremuser:ranking:2021-03-03John
  • 4.展示獲取贊數最多的3個使用者
zrevrangebyrankuser:ranking:2021-03-0302

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後,向所有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節點執行。流程如下:

9.2 哨兵模式

主從模式中,一旦主節點由於故障不能提供服務,需要人工將從節點晉升為主節點,同時還要通知應用方更新主節點地址。顯然,多數業務場景都不能接受這種故障處理方式。Redis從2.8開始正式提供了Redis Sentinel(哨兵)架構來解決這個問題。

哨兵模式,由一個或多個Sentinel例項組成的Sentinel系統,它可以監視所有的Redis主節點和從節點,並在被監視的主節點進入下線狀態時,自動將下線主伺服器屬下的某個從節點升級為新的主節點。但是呢,一個哨兵程序對Redis節點進行監控,就可能會出現問題(單點問題),因此,可以使用多個哨兵來進行監控Redis節點,並且各個哨兵之間還會進行監控。

簡單來說,哨兵模式就三個作用:

  • 傳送命令,等待Redis伺服器(包括主伺服器和從伺服器)返回監控其執行狀態;
  • 哨兵監測到主節點宕機,會自動將從節點切換成主節點,然後通過釋出訂閱模式通知其他的從節點,修改配置檔案,讓它們切換主機;
  • 哨兵之間還會相互監控,從而達到高可用。

故障切換的過程是怎樣的呢

假設主伺服器宕機,哨兵1先檢測到這個結果,系統並不會馬上進行 failover 過程,僅僅是哨兵1主觀的認為主伺服器不可用,這個現象成為主觀下線。當後面的哨兵也檢測到主伺服器不可用,並且數量達到一定值時,那麼哨兵之間就會進行一次投票,投票的結果由一個哨兵發起,進行 failover 操作。切換成功後,就會通過釋出訂閱模式,讓各個哨兵把自己監控的從伺服器實現切換主機,這個過程稱為客觀下線。這樣對於客戶端而言,一切都是透明的。

哨兵的工作模式如下:

  1. 每個Sentinel以每秒鐘一次的頻率向它所知的Master,Slave以及其他Sentinel例項傳送一個 PING命令。
  2. 如果一個例項(instance)距離最後一次有效回覆 PING 命令的時間超過 down-after-milliseconds 選項所指定的值, 則這個例項會被 Sentinel標記為主觀下線。
  3. 如果一個Master被標記為主觀下線,則正在監視這個Master的所有 Sentinel 要以每秒一次的頻率確認Master的確進入了主觀下線狀態。
  4. 當有足夠數量的 Sentinel(大於等於配置檔案指定的值)在指定的時間範圍內確認Master的確進入了主觀下線狀態, 則Master會被標記為客觀下線。
  5. 在一般情況下, 每個 Sentinel 會以每10秒一次的頻率向它已知的所有Master,Slave傳送 INFO 命令。
  6. 當Master被 Sentinel 標記為客觀下線時,Sentinel 向下線的 Master 的所有 Slave 傳送 INFO 命令的頻率會從 10 秒一次改為每秒一次
  7. 若沒有足夠數量的 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指令碼如下:

ifredis.call('get',KEYS[1])==ARGV[1]then
returnredis.call('del',KEYS[1])
else
return0
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. 快取因為某些原因,刪除失敗
  3. 把刪除失敗的key放到訊息佇列
  4. 消費訊息佇列的訊息,獲取要刪除的key
  5. 重試刪除快取操作

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 )
命令描述
EXEC 執行所有事務塊內的命令
DISCARD 取消事務,放棄執行事務塊內的所有命令
MULTI 標記一個事務塊的開始
UNWATCH 取消 WATCH 命令對所有 key 的監視。
WATCH 監視key ,如果在事務執行之前,該key 被其他命令所改動,那麼事務將被打斷。

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自行實現設計也是可以的。


參考資料
[1]

Redis 高可用解決方案總結:https://www.jianshu.com/p/5de2ab291696

[2]

Redia系列九:redis叢集高可用:https://www.cnblogs.com/leeSmall/p/8414687.html

轉自https://mp.weixin.qq.com/s/g8zgPebj830Xesbjk9Lf9g