面試官:聊下Redis的分片叢集,先聊 Redis Cluster好咯?
面試官:Redis Cluser是Redis 3.x才有的官方叢集方案,這塊你瞭解多少?
候選者:嗯,要不還是從基礎講起唄?
候選者:在前面聊Redis的時候,提到的Redis都是「單例項」儲存所有的資料。
候選者:1. 主從模式下實現讀寫分離的架構,可以讓多個從伺服器承載「讀流量」,但面對「寫流量」時,始終是隻有主伺服器在抗。
候選者:2. 「縱向擴充套件」升級Redis伺服器硬體能力,但升級至一定程度下,就不划算了。
候選者:縱向擴充套件意味著「大記憶體」,Redis持久化時的"成本"會加大(Redis做RDB持久化,是全量的,fork子程式時有可能由於使用記憶體過大,導致主執行緒阻塞時間過長)
候選者:所以,「單例項」是有瓶頸的
候選者:「縱向擴充套件」不行,就「橫向擴充套件」唄。
候選者:用多個Redis例項來組成一個叢集,按照一定的規則把資料「分發」到不同的Redis例項上。當叢集所有的Redis例項的資料加起來,那這份資料就是全的
候選者:其實就是「分散式」的概念(:只不過,在Redis裡,好像叫「分片叢集」的人比較多?
候選者:從前面就得知了,要「分散式儲存」,就肯定避免不了對資料進行「分發」(也是路由的意思)
候選者:從Redis Cluster講起吧,它的「路由」是做在客戶端的(SDK已經整合了路由轉發的功能)
候選者:Redis Cluster對資料的分發的邏輯中,涉及到「雜湊槽」(Hash Solt)的概念
候選者:Redis Cluster預設一個叢集有16384個雜湊槽,這些雜湊槽會分配到不同的Redis例項中
候選者:至於怎麼「瓜分」,可以直接均分,也可以「手動」設定每個Redis例項的雜湊槽,全由我們來決定
候選者:重要的是,我們要把這16384個都得瓜分完,不能有剩餘!
候選者:當客戶端有資料進行寫入的時候,首先會對key按照CRC16演算法計算出16bit的值(可以理解為就是做hash),然後得到的值對16384進行取模
候選者:取模之後,自然就得到其中一個雜湊槽,然後就可以將資料插入到分配至該雜湊槽的Redis例項中
面試官:那問題就來了,現在客戶端通過hash演算法算出了雜湊槽的位置,那客戶端怎麼知道這個雜湊槽在哪臺Redis例項上呢?
候選者:是這樣的,在叢集的中每個Redis例項都會向其他例項「傳播」自己所負責的雜湊槽有哪些。這樣一來,每臺Redis例項就可以記錄著「所有雜湊槽與例項」的關係了(:
候選者:有了這個對映關係以後,客戶端也會「快取」一份到自己的本地上,那自然客戶端就知道去哪個Redis例項上操作了
面試官:那我又有問題了,在叢集裡也可以新增或者刪除Redis例項啊,這個怎麼整?
候選者:當叢集刪除或者新增Redis例項時,那總會有某Redis例項所負責的雜湊槽關係會發生變化
候選者:發生變化的資訊會通過訊息傳送至整個叢集中,所有的Redis例項都會知道該變化,然後更新自己所儲存的對映關係
候選者:但這時候,客戶端其實是不感知的(:
候選者:所以,當客戶端請求時某Key時,還是會請求到「原來」的Redis例項上。而原來的Redis例項會返回「moved」命令,告訴客戶端應該要去新的Redis例項上去請求啦
候選者:客戶端接收到「moved」命令之後,就知道去新的Redis例項請求了,並且更新「快取雜湊槽與例項之間的對映關係」
候選者:總結起來就是:資料遷移完畢後被響應,客戶端會收到「moved」命令,並且會更新本地快取
面試官:那資料還沒完全遷移完呢?
候選者:如果資料還沒完全遷移完,那這時候會返回客戶端「ask」命令。也是讓客戶端去請求新的Redis例項,但客戶端這時候不會更新本地快取
面試官:瞭解了
面試官:說白了就是,如果叢集Redis例項存在變動,由於Redis例項之間會「通訊」
面試官:所以等到客戶端請求時,Redis例項總會知道客戶端所要請求的資料在哪個Redis例項上
面試官:如果已經遷移完畢了,那就返回「move」命令告訴客戶端應該去找哪個Redis例項要資料,並且客戶端應該更新自己的快取(對映關係)
面試官:如果正在遷移中,那就返回「ack」命令告訴客戶端應該去找哪個Redis例項要資料
候選者:不愧是你...
面試官:那你知道為什麼雜湊槽是16384個嗎?
候選者:嗯,這個。是這樣的,Redis例項之間「通訊」會相互交換「槽資訊」,那如果槽過多(意味著網路包會變大),網路包變大,那是不是就意味著會「過度佔用」網路的頻寬
候選者:另外一塊是,Redis作者認為叢集在一般情況下是不會超過1000個例項
候選者:那就取了16384個,即可以將資料合理打散至Redis叢集中的不同例項,又不會在交換資料時導致頻寬佔用過多
面試官:瞭解了
面試官:那你知道為什麼對資料進行分割槽在Redis中用的是「雜湊槽」這種方式嗎?而不是一致性雜湊演算法
候選者:在我理解下,一致性雜湊演算法就是有個「雜湊環」,當客戶端請求時,會對Key進行hash,確定在雜湊環上的位置,然後順時針往後找,找到的第一個真實節點
候選者:一致性雜湊演算法比「傳統固定取模」的好處就是:如果叢集中需要新增或刪除某例項,只會影響一小部分的資料
候選者:但如果在叢集中新增或者刪除例項,在一致性雜湊演算法下,就得知道是「哪一部分資料」受到影響了,需要進行對受影響的資料進行遷移
面試官:嗯...
候選者:而雜湊槽的方式,我們通過上面已經可以發現:在叢集中的每個例項都能拿到槽位相關的資訊
候選者:當客戶端對key進行hash運算之後,如果發現請求的例項沒有相關的資料,例項會返回「重定向」命令告訴客戶端應該去哪兒請求
候選者:叢集的擴容、縮容都是以「雜湊槽」作為基本單位進行操作,總的來說就是「實現」會更加簡單(簡潔,高效,有彈性)。過程大概就是把部分槽進行重新分配,然後遷移槽中的資料即可,不會影響到叢集中某個例項的所有資料。
面試官:那你瞭解「服務端 路由」的大致原理嗎?
候選者:嗯,服務端路由一般指的就是,有個代理層專門對接客戶端的請求,然後再轉發到Redis叢集進行處理
候選者:上次最後面試的時候,也提到了,現在比較流行的是Codis
候選者:它與Redis Cluster最大的區別就是,Redis Cluster是直連Redis例項的,而Codis則客戶端直連Proxy,再由Proxy進行分發到不同的Redis例項進行處理
候選者:在Codis對Key路由的方案跟Redis Cluster很類似,Codis初始化出1024個雜湊槽,然後分配到不同的Redis伺服器中
候選者:雜湊槽與Redis例項的對映關係由Zookeeper進行儲存和管理,Proxy會通過Codis DashBoard得到最新的對映關係,並快取在本地上
面試官:那如果我要擴容Codis Redis例項的流程是怎麼樣的?
候選者:簡單來說就是:把新的Redis例項加入到叢集中,然後把部分資料遷移到新的例項上
候選者:大概的過程就是:1.「原例項」某一個Solt的部分資料傳送給「目標例項」。2.「目標例項」收到資料後,給「原例項」返回ack。3.「原例項」收到ack之後,在本地刪除掉剛剛給「目標例項」的資料。4.不斷迴圈1、2、3步驟,直至整個solt遷移完畢
候選者:Codis也是支援「非同步遷移」的,針對上面的步驟2,「原例項」傳送資料後,不等待「目標例項」返回ack,就繼續接收客戶端的請求。
候選者:未遷移完的資料標記為「只讀」,不會影響到資料的一致性。如果對遷移中的資料存在「寫操作」,那會讓客戶端進行「重試」,最後會寫到「目標例項」上
候選者:還有就是,針對 bigkey,非同步遷移採用了「拆分指令」的方式進行遷移,比如有個set元素有10000個,那「原例項」可能就傳送10000條命令給「目標例項」,而不是一整個bigkey一次性遷移(因為大物件容易造成阻塞)
面試官:瞭解了。
本文總結:
-
分片叢集誕生理由:寫效能在高併發下會遇到瓶頸&&無法無限地縱向擴充套件(不划算)
-
分片叢集:需要解決「資料路由」和「資料遷移」的問題
-
Redis Cluster資料路由:
- Redis Cluster預設一個叢集有16384個雜湊槽,雜湊槽會被分配到Redis叢集中的例項中
- Redis叢集的例項會相互「通訊」,互動自己所負責雜湊槽資訊(最終每個例項都有完整的對映關係)
- 當客戶端請求時,使用CRC16演算法算出Hash值並模以16384,自然就能得到雜湊槽進而得到所對應的Redis例項位置
-
為什麼16384個雜湊槽:16384個既能讓Redis例項分配到的資料相對均勻,又不會影響Redis例項之間互動槽資訊產生嚴重的網路效能開銷問題
-
Redis Cluster 為什麼使用雜湊槽,而非一致性雜湊演算法:雜湊槽實現相對簡單高效,每次擴縮容只需要動對應Solt(槽)的資料,一般不會動整個Redis例項
-
Codis資料路由:預設分配1024個雜湊槽,對映相關資訊會被儲存至Zookeeper叢集。Proxy會快取一份至本地,Redis叢集例項發生變化時,DashBoard更新Zookeeper和Proxy的對映資訊
-
Redis Cluster和Codis資料遷移:Redis Cluster支援同步遷移,Codis支援同步遷移&&非同步遷移
- 把新的Redis例項加入到叢集中,然後把部分資料遷移到新的例項上(線上)
歡迎關注我的微信公眾號【Java3y】來聊聊Java面試,對線面試官系列持續更新中!
【對線面試官-移動端】系列 一週兩篇持續更新中!
【對線面試官-電腦端】系列 一週兩篇持續更新中!
原創不易!!求三連!!