7.redis 集群模式的工作原理能說一下么?在集群模式下,redis 的 key 是如何尋址的?分布式尋址都有哪些算法?了解一致性 hash 算法嗎?考點分析在前幾年,redis 如果要搞幾個節(jié)點,每個節(jié)點存儲一部分的數(shù)據(jù),得借助一些中間件來實現(xiàn),比如說有 這兩年,redis 不斷在發(fā)展,redis 也不斷的有新的版本,現(xiàn)在的 redis 集群模式,可以做到在多臺機器上,部署多個 redis 實例,每個實例存儲一部分的數(shù)據(jù),同時每個 redis 實例可以掛 redis 從實例,自動確保說,如果 redis 主實例掛了,會自動切換到 redis 從實例頂上來。 現(xiàn)在 redis 的新版本,大家都是用 redis cluster 的,也就是 redis 原生支持的 redis 集群模式,那么面試官肯定會就 redis cluster 對你來個幾連炮。要是你沒用過 redis cluster,正常,以前很多人用 codis 之類的客戶端來支持集群,但是起碼你得研究一下 redis cluster 吧。 如果你的數(shù)據(jù)量很少,主要是承載高并發(fā)高性能的場景,比如你的緩存一般就幾個 G,單機就足夠了,可以使用 replication,一個 master 多個 slaves,要幾個 slave 跟你要求的讀吞吐量有關(guān),然后自己搭建一個 sentinel 集群去保證 redis 主從架構(gòu)的高可用性。 redis cluster,主要是針對海量數(shù)據(jù)+高并發(fā)+高可用的場景。redis cluster 支撐 N 個 redis master node,每個 master node 都可以掛載多個 slave node。這樣整個 redis 就可以橫向擴容了。如果你要支撐更大數(shù)據(jù)量的緩存,那就橫向擴容更多的 master 節(jié)點,每個 master 節(jié)點就能存放更多的數(shù)據(jù)了。 面試題剖析redis cluster 介紹
在 redis cluster 架構(gòu)下,每個 redis 要放開兩個端口號,比如一個是 6379,另外一個就是 加1w 的端口號,比如 16379。 16379 端口號是用來進行節(jié)點間通信的,也就是 cluster bus 的東西,cluster bus 的通信,用來進行故障檢測、配置更新、故障轉(zhuǎn)移授權(quán)。cluster bus 用了另外一種二進制的協(xié)議, 節(jié)點間的內(nèi)部通信機制基本通信原理
![]() zookeeper-centralized-storage redis 維護集群元數(shù)據(jù)采用另一個方式, ![]() redis-gossip 集中式的好處在于,元數(shù)據(jù)的讀取和更新,時效性非常好,一旦元數(shù)據(jù)出現(xiàn)了變更,就立即更新到集中式的存儲中,其它節(jié)點讀取的時候就可以感知到;不好在于,所有的元數(shù)據(jù)的更新壓力全部集中在一個地方,可能會導致元數(shù)據(jù)的存儲有壓力。 gossip 好處在于,元數(shù)據(jù)的更新比較分散,不是集中在一個地方,更新請求會陸陸續(xù)續(xù),打到所有節(jié)點上去更新,降低了壓力;不好在于,元數(shù)據(jù)的更新有延時,可能導致集群中的一些操作會有一些滯后。
gossip 協(xié)議gossip 協(xié)議包含多種消息,包含
redis-trib.rb add-node 其實內(nèi)部就是發(fā)送了一個 gossip meet 消息給新加入的節(jié)點,通知那個節(jié)點去加入我們的集群。
ping 消息深入ping 時要攜帶一些元數(shù)據(jù),如果很頻繁,可能會加重網(wǎng)絡負擔。 每個節(jié)點每秒會執(zhí)行 10 次 ping,每次會選擇 5 個最久沒有通信的其它節(jié)點。當然如果發(fā)現(xiàn)某個節(jié)點通信延時達到了 每次 ping,會帶上自己節(jié)點的信息,還有就是帶上 1/10 其它節(jié)點的信息,發(fā)送出去,進行交換。至少包含 分布式尋址算法
hash 算法來了一個 key,首先計算 hash 值,然后對節(jié)點數(shù)取模。然后打在不同的 master 節(jié)點上。一旦某一個 master 節(jié)點宕機,所有請求過來,都會基于最新的剩余 master 節(jié)點數(shù)去取模,嘗試去取數(shù)據(jù)。這會導致大部分的請求過來,全部無法拿到有效的緩存,導致大量的流量涌入數(shù)據(jù)庫。 ![]() hash 一致性 hash 算法一致性 hash 算法將整個 hash 值空間組織成一個虛擬的圓環(huán),整個空間按順時針方向組織,下一步將各個 master 節(jié)點(使用服務器的 ip 或主機名)進行 hash。這樣就能確定每個節(jié)點在其哈希環(huán)上的位置。 來了一個 key,首先計算 hash 值,并確定此數(shù)據(jù)在環(huán)上的位置,從此位置沿環(huán)順時針“行走”,遇到的第一個 master 節(jié)點就是 key 所在位置。 在一致性哈希算法中,如果一個節(jié)點掛了,受影響的數(shù)據(jù)僅僅是此節(jié)點到環(huán)空間前一個節(jié)點(沿著逆時針方向行走遇到的第一個節(jié)點)之間的數(shù)據(jù),其它不受影響。增加一個節(jié)點也同理。 燃鵝,一致性哈希算法在節(jié)點太少時,容易因為節(jié)點分布不均勻而造成緩存熱點的問題。為了解決這種熱點問題,一致性 hash 算法引入了虛擬節(jié)點機制,即對每一個節(jié)點計算多個 hash,每個計算結(jié)果位置都放置一個虛擬節(jié)點。這樣就實現(xiàn)了數(shù)據(jù)的均勻分布,負載均衡。 ![]() consistent-hashing-algorithm redis cluster 的 hash slot 算法redis cluster 有固定的 redis cluster 中每個 master 都會持有部分 slot,比如有 3 個 master,那么可能每個 master 持有 5000 多個 hash slot。hash slot 讓 node 的增加和移除很簡單,增加一個 master,就將其他 master 的 hash slot 移動部分過去,減少一個 master,就將它的 hash slot 移動到其他 master 上去。移動 hash slot 的成本是非常低的。客戶端的 api,可以對指定的數(shù)據(jù),讓他們走同一個 hash slot,通過 任何一臺機器宕機,另外兩個節(jié)點,不影響的。因為 key 找的是 hash slot,不是機器。 ![]() hash-slot redis cluster 的高可用與主備切換原理redis cluster 的高可用的原理,幾乎跟哨兵是類似的 判斷節(jié)點宕機如果一個節(jié)點認為另外一個節(jié)點宕機,那么就是 在 如果一個節(jié)點認為某個節(jié)點 從節(jié)點過濾對宕機的 master node,從其所有的 slave node 中,選擇一個切換成 master node。 檢查每個 slave node 與 master node 斷開連接的時間,如果超過了 從節(jié)點選舉每個從節(jié)點,都根據(jù)自己對 master 復制數(shù)據(jù)的 offset,來設置一個選舉時間,offset 越大(復制數(shù)據(jù)越多)的從節(jié)點,選舉時間越靠前,優(yōu)先進行選舉。 所有的 master node 開始 slave 選舉投票,給要進行選舉的 slave 進行投票,如果大部分 master node 從節(jié)點執(zhí)行主備切換,從節(jié)點切換為主節(jié)點。 與哨兵比較整個流程跟哨兵相比,非常類似,所以說,redis cluster 功能強大,直接集成了 replication 和 sentinel 的功能。 8.了解什么是 redis 的雪崩和穿透?redis 崩潰之后會怎么樣?系統(tǒng)該如何應對這種情況?如何處理 redis 的穿透?面試題剖析緩存雪崩對于系統(tǒng) A,假設每天高峰期每秒 5000 個請求,本來緩存在高峰期可以扛住每秒 4000 個請求,但是緩存機器意外發(fā)生了全盤宕機。緩存掛了,此時 1 秒 5000 個請求全部落數(shù)據(jù)庫,數(shù)據(jù)庫必然扛不住,它會報一下警,然后就掛了。此時,如果沒用什么特別的方案來處理這個故障,DBA 很著急,重啟數(shù)據(jù)庫,但是數(shù)據(jù)庫立馬又被新的流量給打死了。 這就是緩存雪崩。 ![]() redis-caching-avalanche 大約在 3 年前,國內(nèi)比較知名的一個互聯(lián)網(wǎng)公司,曾因為緩存事故,導致雪崩,后臺系統(tǒng)全部崩潰,事故從當天下午持續(xù)到晚上凌晨 3~4 點,公司損失了幾千萬。 緩存雪崩的事前事中事后的解決方案如下。
![]() redis-caching-avalanche-solution 用戶發(fā)送一個請求,系統(tǒng) A 收到請求后,先查本地 ehcache 緩存,如果沒查到再查 redis。如果 ehcache 和 redis 都沒有,再查數(shù)據(jù)庫,將數(shù)據(jù)庫中的結(jié)果,寫入 ehcache 和 redis 中。 限流組件,可以設置每秒的請求,有多少能通過組件,剩余的未通過的請求,怎么辦?走降級!可以返回一些默認的值,或者友情提示,或者空白的值。 好處:
緩存穿透對于系統(tǒng)A,假設一秒 5000 個請求,結(jié)果其中 4000 個請求是黑客發(fā)出的惡意攻擊。 黑客發(fā)出的那 4000 個攻擊,緩存中查不到,每次你去數(shù)據(jù)庫里查,也查不到。 舉個栗子。數(shù)據(jù)庫 id 是從 1 開始的,結(jié)果黑客發(fā)過來的請求 id 全部都是負數(shù)。這樣的話,緩存中不會有,請求每次都“視緩存于無物”,直接查詢數(shù)據(jù)庫。這種惡意攻擊場景的緩存穿透就會直接把數(shù)據(jù)庫給打死。 ![]() redis-caching-penetration 解決方式很簡單,每次系統(tǒng) A 從數(shù)據(jù)庫中只要沒查到,就寫一個空值到緩存里去,比如 9.如何保證緩存與數(shù)據(jù)庫的雙寫一致性?面試題剖析一般來說,如果允許緩存可以稍微的跟數(shù)據(jù)庫偶爾有不一致的情況,也就是說如果你的系統(tǒng)不是嚴格要求 “緩存+數(shù)據(jù)庫” 必須保持一致性的話,最好不要做這個方案,即:讀請求和寫請求串行化,串到一個內(nèi)存隊列里去。 串行化可以保證一定不會出現(xiàn)不一致的情況,但是它也會導致系統(tǒng)的吞吐量大幅度降低,用比正常情況下多幾倍的機器去支撐線上的一個請求。 Cache Aside Pattern最經(jīng)典的緩存+數(shù)據(jù)庫讀寫的模式,就是 Cache Aside Pattern。
為什么是刪除緩存,而不是更新緩存? 原因很簡單,很多時候,在復雜點的緩存場景,緩存不單單是數(shù)據(jù)庫中直接取出來的值。 比如可能更新了某個表的一個字段,然后其對應的緩存,是需要查詢另外兩個表的數(shù)據(jù)并進行運算,才能計算出緩存最新的值的。 另外更新緩存的代價有時候是很高的。是不是說,每次修改數(shù)據(jù)庫的時候,都一定要將其對應的緩存更新一份?也許有的場景是這樣,但是對于比較復雜的緩存數(shù)據(jù)計算的場景,就不是這樣了。如果你頻繁修改一個緩存涉及的多個表,緩存也頻繁更新。但是問題在于,這個緩存到底會不會被頻繁訪問到? 舉個栗子,一個緩存涉及的表的字段,在 1 分鐘內(nèi)就修改了 20 次,或者是 100 次,那么緩存更新 20 次、100 次;但是這個緩存在 1 分鐘內(nèi)只被讀取了 1 次,有大量的冷數(shù)據(jù)。實際上,如果你只是刪除緩存的話,那么在 1 分鐘內(nèi),這個緩存不過就重新計算一次而已,開銷大幅度降低。用到緩存才去算緩存。 其實刪除緩存,而不是更新緩存,就是一個 lazy 計算的思想,不要每次都重新做復雜的計算,不管它會不會用到,而是讓它到需要被使用的時候再重新計算。像 mybatis,hibernate,都有懶加載思想。查詢一個部門,部門帶了一個員工的 list,沒有必要說每次查詢部門,都里面的 1000 個員工的數(shù)據(jù)也同時查出來啊。80% 的情況,查這個部門,就只是要訪問這個部門的信息就可以了。先查部門,同時要訪問里面的員工,那么這個時候只有在你要訪問里面的員工的時候,才會去數(shù)據(jù)庫里面查詢 1000 個員工。 最初級的緩存不一致問題及解決方案問題:先修改數(shù)據(jù)庫,再刪除緩存。如果刪除緩存失敗了,那么會導致數(shù)據(jù)庫中是新數(shù)據(jù),緩存中是舊數(shù)據(jù),數(shù)據(jù)就出現(xiàn)了不一致。 ![]() redis-junior-inconsistent 解決思路:先刪除緩存,再修改數(shù)據(jù)庫。如果數(shù)據(jù)庫修改失敗了,那么數(shù)據(jù)庫中是舊數(shù)據(jù),緩存中是空的,那么數(shù)據(jù)不會不一致。因為讀的時候緩存沒有,則讀數(shù)據(jù)庫中舊數(shù)據(jù),然后更新到緩存中。 比較復雜的數(shù)據(jù)不一致問題分析數(shù)據(jù)發(fā)生了變更,先刪除了緩存,然后要去修改數(shù)據(jù)庫,此時還沒修改。一個請求過來,去讀緩存,發(fā)現(xiàn)緩存空了,去查詢數(shù)據(jù)庫,查到了修改前的舊數(shù)據(jù),放到了緩存中。隨后數(shù)據(jù)變更的程序完成了數(shù)據(jù)庫的修改。完了,數(shù)據(jù)庫和緩存中的數(shù)據(jù)不一樣了... 為什么上億流量高并發(fā)場景下,緩存會出現(xiàn)這個問題? 只有在對一個數(shù)據(jù)在并發(fā)的進行讀寫的時候,才可能會出現(xiàn)這種問題。其實如果說你的并發(fā)量很低的話,特別是讀并發(fā)很低,每天訪問量就 1 萬次,那么很少的情況下,會出現(xiàn)剛才描述的那種不一致的場景。但是問題是,如果每天的是上億的流量,每秒并發(fā)讀是幾萬,每秒只要有數(shù)據(jù)更新的請求,就可能會出現(xiàn)上述的數(shù)據(jù)庫+緩存不一致的情況。 解決方案如下: 更新數(shù)據(jù)的時候,根據(jù)數(shù)據(jù)的唯一標識,將操作路由之后,發(fā)送到一個 jvm 內(nèi)部隊列中。讀取數(shù)據(jù)的時候,如果發(fā)現(xiàn)數(shù)據(jù)不在緩存中,那么將重新讀取數(shù)據(jù)+更新緩存的操作,根據(jù)唯一標識路由之后,也發(fā)送同一個 jvm 內(nèi)部隊列中。 一個隊列對應一個工作線程,每個工作線程串行拿到對應的操作,然后一條一條的執(zhí)行。這樣的話,一個數(shù)據(jù)變更的操作,先刪除緩存,然后再去更新數(shù)據(jù)庫,但是還沒完成更新。此時如果一個讀請求過來,讀到了空的緩存,那么可以先將緩存更新的請求發(fā)送到隊列中,此時會在隊列中積壓,然后同步等待緩存更新完成。 這里有一個優(yōu)化點,一個隊列中,其實多個更新緩存請求串在一起是沒意義的,因此可以做過濾,如果發(fā)現(xiàn)隊列中已經(jīng)有一個更新緩存的請求了,那么就不用再放個更新請求操作進去了,直接等待前面的更新操作請求完成即可。 待那個隊列對應的工作線程完成了上一個操作的數(shù)據(jù)庫的修改之后,才會去執(zhí)行下一個操作,也就是緩存更新的操作,此時會從數(shù)據(jù)庫中讀取最新的值,然后寫入緩存中。 如果請求還在等待時間范圍內(nèi),不斷輪詢發(fā)現(xiàn)可以取到值了,那么就直接返回;如果請求等待的時間超過一定時長,那么這一次直接從數(shù)據(jù)庫中讀取當前的舊值。 高并發(fā)的場景下,該解決方案要注意的問題:
由于讀請求進行了非常輕度的異步化,所以一定要注意讀超時的問題,每個讀請求必須在超時時間范圍內(nèi)返回。 該解決方案,最大的風險點在于說,可能數(shù)據(jù)更新很頻繁,導致隊列中積壓了大量更新操作在里面,然后讀請求會發(fā)生大量的超時,最后導致大量的請求直接走數(shù)據(jù)庫。務必通過一些模擬真實的測試,看看更新數(shù)據(jù)的頻率是怎樣的。 另外一點,因為一個隊列中,可能會積壓針對多個數(shù)據(jù)項的更新操作,因此需要根據(jù)自己的業(yè)務情況進行測試,可能需要部署多個服務,每個服務分攤一些數(shù)據(jù)的更新操作。如果一個內(nèi)存隊列里居然會擠壓 100 個商品的庫存修改操作,每隔庫存修改操作要耗費 10ms 去完成,那么最后一個商品的讀請求,可能等待 10 * 100 = 1000ms = 1s 后,才能得到數(shù)據(jù),這個時候就導致讀請求的長時阻塞。 一定要做根據(jù)實際業(yè)務系統(tǒng)的運行情況,去進行一些壓力測試,和模擬線上環(huán)境,去看看最繁忙的時候,內(nèi)存隊列可能會擠壓多少更新操作,可能會導致最后一個更新操作對應的讀請求,會 hang 多少時間,如果讀請求在 200ms 返回,如果你計算過后,哪怕是最繁忙的時候,積壓 10 個更新操作,最多等待 200ms,那還可以的。 如果一個內(nèi)存隊列中可能積壓的更新操作特別多,那么你就要加機器,讓每個機器上部署的服務實例處理更少的數(shù)據(jù),那么每個內(nèi)存隊列中積壓的更新操作就會越少。 其實根據(jù)之前的項目經(jīng)驗,一般來說,數(shù)據(jù)的寫頻率是很低的,因此實際上正常來說,在隊列中積壓的更新操作應該是很少的。像這種針對讀高并發(fā)、讀緩存架構(gòu)的項目,一般來說寫請求是非常少的,每秒的 QPS 能到幾百就不錯了。 我們來實際粗略測算一下。 如果一秒有 500 的寫操作,如果分成 5 個時間片,每 200ms 就 100 個寫操作,放到 20 個內(nèi)存隊列中,每個內(nèi)存隊列,可能就積壓 5 個寫操作。每個寫操作性能測試后,一般是在 20ms 左右就完成,那么針對每個內(nèi)存隊列的數(shù)據(jù)的讀請求,也就最多 hang 一會兒,200ms 以內(nèi)肯定能返回了。 經(jīng)過剛才簡單的測算,我們知道,單機支撐的寫 QPS 在幾百是沒問題的,如果寫 QPS 擴大了 10 倍,那么就擴容機器,擴容 10 倍的機器,每個機器 20 個隊列。
這里還必須做好壓力測試,確保恰巧碰上上述情況的時候,還有一個風險,就是突然間大量讀請求會在幾十毫秒的延時 hang 在服務上,看服務能不能扛的住,需要多少機器才能扛住最大的極限情況的峰值。 但是因為并不是所有的數(shù)據(jù)都在同一時間更新,緩存也不會同一時間失效,所以每次可能也就是少數(shù)數(shù)據(jù)的緩存失效了,然后那些數(shù)據(jù)對應的讀請求過來,并發(fā)量應該也不會特別大。
可能這個服務部署了多個實例,那么必須保證說,執(zhí)行數(shù)據(jù)更新操作,以及執(zhí)行緩存更新操作的請求,都通過 Nginx 服務器路由到相同的服務實例上。 比如說,對同一個商品的讀寫請求,全部路由到同一臺機器上??梢宰约喝プ龇臻g的按照某個請求參數(shù)的 hash 路由,也可以用 Nginx 的 hash 路由功能等等。
萬一某個商品的讀寫請求特別高,全部打到相同的機器的相同的隊列里面去了,可能會造成某臺機器的壓力過大。就是說,因為只有在商品數(shù)據(jù)更新的時候才會清空緩存,然后才會導致讀寫并發(fā),所以其實要根據(jù)業(yè)務系統(tǒng)去看,如果更新頻率不是太高的話,這個問題的影響并不是特別大,但是的確可能某些機器的負載會高一些。 10.redis 的并發(fā)競爭問題是什么?如何解決這個問題?了解 redis 事務的 CAS 方案嗎?考點分析這個也是線上非常常見的一個問題,就是多客戶端同時并發(fā)寫一個 key,可能本來應該先到的數(shù)據(jù)后到了,導致數(shù)據(jù)版本錯了;或者是多客戶端同時獲取一個 key,修改值之后再寫回去,只要順序錯了,數(shù)據(jù)就錯了。 而且 redis 自己就有天然解決這個問題的 CAS 類的樂觀鎖方案。 面試題剖析某個時刻,多個系統(tǒng)實例都去更新某個 key??梢曰?zookeeper 實現(xiàn)分布式鎖。每個系統(tǒng)通過 zookeeper 獲取分布式鎖,確保同一時間,只能有一個系統(tǒng)實例在操作某個 key,別人都不允許讀和寫。 ![]() zookeeper-distributed-lock 你要寫入緩存的數(shù)據(jù),都是從 mysql 里查出來的,都得寫入 mysql 中,寫入 mysql 中的時候必須保存一個時間戳,從 mysql 查出來的時候,時間戳也查出來。 每次要寫之前,先判斷一下當前這個 value 的時間戳是否比緩存里的 value 的時間戳要新。如果是的話,那么可以寫,否則,就不能用舊的數(shù)據(jù)覆蓋新的數(shù)據(jù)。 11.生產(chǎn)環(huán)境中的 redis 是怎么部署的?面試題剖析redis cluster,10 臺機器,5 臺機器部署了 redis 主實例,另外 5 臺機器部署了 redis 的從實例,每個主實例掛了一個從實例,5 個節(jié)點對外提供讀寫服務,每個節(jié)點的讀寫高峰qps可能可以達到每秒 5 萬,5 臺機器最多是 25 萬讀寫請求/s。 機器是什么配置?32G 內(nèi)存+ 8 核 CPU + 1T 磁盤,但是分配給 redis 進程的是10g內(nèi)存,一般線上生產(chǎn)環(huán)境,redis 的內(nèi)存盡量不要超過 10g,超過 10g 可能會有問題。 5 臺機器對外提供讀寫,一共有 50g 內(nèi)存。 因為每個主實例都掛了一個從實例,所以是高可用的,任何一個主實例宕機,都會自動故障遷移,redis 從實例會自動變成主實例繼續(xù)提供讀寫服務。 你往內(nèi)存里寫的是什么數(shù)據(jù)?每條數(shù)據(jù)的大小是多少?商品數(shù)據(jù),每條數(shù)據(jù)是 10kb。100 條數(shù)據(jù)是 1mb,10 萬條數(shù)據(jù)是 1g。常駐內(nèi)存的是 200 萬條商品數(shù)據(jù),占用內(nèi)存是 20g,僅僅不到總內(nèi)存的 50%。目前高峰期每秒就是 3500 左右的請求量。 其實大型的公司,會有基礎架構(gòu)的 team 負責緩存集群的運維。
? 著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者 |
|