Redis中如何處理熱key問題?以下這篇文章就來跟大家介紹一下Redis快取熱key問題的常用解決方案,希望對大家有幫助!
做一些C端業務,不可避免的要引入一級快取來代替資料庫的壓力並且減少業務響應時間,其實每次引入一個中間件來解決問題的同時,必然會帶來許多新的問題需要注意,例如上篇文章《資料庫與快取一致性實戰》中提到的如何做快取的一致性。那麼其實還會有一些其他問題例如使用Redis作為一級快取時可能帶來的熱key、大key等問題,本文我們就熱key(hot key)
問題來討論,如何合理的解決熱key
問題。
熱key
是什麼問題,如何造成的?
一般來說,我們使用的快取Redis都是多節點的叢集版,對某個key進行讀寫時,會根據該key的hash計算出對應的slot,根據這個slot就能找到與之對應的分片(一個master和多個slave組成的一組redis集群)來存取該K-V。但在實際應用過程中,對於某些特定業務或一些特定的時段(例如電商業務的商品秒殺活動),可能會發生大量的請求訪問同一個key。所有的請求(且這類請求讀寫比例非常高)都會落到同一個redis server上,該redis的負載就會嚴重加劇,此時整個系統增加新redis實例也沒有任何用處,因為根據hash演算法,同一個key的請求還是會落到同一台新機器上,該機器依然會成為系統瓶頸2,甚至造成整個集群宕掉,若此熱點key的value 也比較大,也會造成網卡達到瓶頸,這種問題稱為“熱key” 問題。 【相關推薦:Redis影片教學】
如下圖1、2所示,分別是正常redis cluster叢集和使用一層proxy代理程式的redis 叢集key存取。
如上所說,熱key會為叢集中的少數節點帶來超高的負載壓力,如果不正確處理,那麼這些節點宕機都有可能,從而影響整個快取叢集的運作,因此我們必須及時發現熱key、解決熱key問題。
熱key探測,看到由於redis群聚的分散性以及熱點key帶來的一些顯著影響,我們可以透過由粗及細的思考流程來做熱點key探測的方案。
熱key最明顯的影響是整個redis叢集中的qps並沒有那麼大的前提下,流量分佈在叢集中slot不均的問題,那麼我們可以最先想到的就是對於每個slot中的流量做監控,上報之後做每個slot的流量對比,就能在熱key出現時發現影響到的具體slot 。雖然這個監控最為方便,但是粒度過於粗了,僅適用於前期群集監控方案,並不適用於精準偵測到熱key的場景。
如果我們使用的是圖2的redis叢集proxy代理模式,由於所有的請求都會先到proxy再到具體的slot節點,那麼這個熱點key的探測統計就可以放在proxy中做,在proxy中基於時間滑動視窗
,對每個key做計數,然後統計出超出對應閾值的key。為了防止過多冗餘的統計,也可以設定一些規則,僅統計對應前綴和類型的key。這種方式需要至少有proxy的代理機制,對於redis架構有要求。
redis 4.0以上的版本支援了每個節點上的基於LFU的熱點key發現機制,使用redis-cli –hotkeys
即可,執行redis-cli時加上–hotkeys選項。可以定時在節點中使用該指令來發現對應熱點key。
如下圖所示,可以看到redis-cli –hotkeys
的執行結果,熱key的統計訊息,這個指令的執行時間較長,可以設定定時執行來統計。
由於redis的指令每次都是從客戶端發出,基於此我們可以在redis client的一些程式碼處進行統計計數,每個client做基於時間滑動視窗的統計,超過一定的閾值之後上報至server,然後統一由server下發至各個client,並且配置對應的過期時間。
這個方式看起來更優美
,其實在某些應用場景中並不是那麼合適,因為在client端這一側的改造,會為運行的進程帶來更大的記憶體開銷,更直接的來說,對於Java和goLang這種自動記憶體管理的語言,會更加頻繁的創建對象,從而觸發gc導致接口響應耗時增加的問題,這個反而是不太容易預料到的事情。
最後可以透過各公司的基礎建設,做出對應的選擇。
透過上述幾種方式我們偵測到了對應熱key或熱slot,那麼我們就要解決對應的熱key問題。解決熱key也有好幾個思路可以參考,我們一個一個捋一下。
一種最簡單粗暴的方式,對於特定的slot或熱key做限流,這個方案明顯對於業務來說是有損的,所以建議只用在出現線上問題,需要停損的時候進行特定的限流。
本地快取也是一個最常用的解決方案,既然我們的一級快取扛不住這麼大的壓力,就再加一個二級緩存吧。由於每個請求都是由service發出的,這個二級快取加在service端是再合適不過了,因此可以在服務端每次獲取到對應熱key時,使用本地緩存存儲一份,等本地緩存過期後再重新請求,降低redis集群壓力。以java為例,guavaCache就是現成的工具。以下範例:
//本地缓存初始化以及构造 private static LoadingCache<String, List<Object>> configCache = CacheBuilder.newBuilder() .concurrencyLevel(8) //并发读写的级别,建议设置cpu核数 .expireAfterWrite(10, TimeUnit.SECONDS) //写入数据后多久过期 .initialCapacity(10) //初始化cache的容器大小 .maximumSize(10)//cache的容器最大 .recordStats() // build方法中可以指定CacheLoader,在缓存不存在时通过CacheLoader的实现自动加载缓存 .build(new CacheLoader<String, List<Object>>() { @Override public List<Object> load(String hotKey) throws Exception { } }); //本地缓存获取 Object result = configCache.get(key);
本地快取對於我們的最大的影響就是資料不一致的問題,我們設定多長的快取過期時間,就會導致最長有多久的線上資料不一致問題,這個快取時間需要衡量自身的集群壓力以及業務接受的最大不一致時間。
如何確保不出現熱key問題,又能盡量的保證資料一致性?拆key也是一個好的解決方案。
我們放入快取時將對應業務的快取key分割成多個不同的key。如下圖所示,我們首先在更新緩存的一側,將key拆成N份,例如一個key名字叫做"good_100",那我們就可以把它拆成四份,"good_100_copy1"、"good_100_copy2"、" good_100_copy3"、"good_100_copy4",每次更新新增時都需要去改動這N個key,這一步就是拆key。
對service端來講,我們就需要想辦法盡量將自己訪問的流量足夠的均勻,如何為自己即將訪問的熱key上加入後綴。幾個辦法,依照本機的ip或mac位址做hash,之後的值與拆key的數量做取餘,最後決定拼接成什麼樣的key字尾,從而打到哪台機器上;服務啟動時的一個隨機數字對拆key的數量做取餘。
對於熟悉微服務設定中心的夥伴來講,我們的思路可以向配置中心的一致性轉變一下。拿nacos來舉例,它是如何做到分散式的配置一致性的,並且對應速度很快?那我們可以將快取類比配置,這樣去做。
長輪詢
本地化
的設定。首先服務啟動時會初始化全部的配置,然後定時啟動長輪詢去查詢當前服務監聽的配置有沒有變更,如果有變更,長輪詢的請求便會立刻返回,更新本地配置;如果沒有變更,對於所有的業務代碼都是使用本地的記憶體快取配置。這樣就能保證分散式的快取配置時效性與一致性。
上述的每個方案都相對獨立的去解決熱key問題,那麼如果我們真的在面臨業務訴求時,其實會有很長的時間來考慮整體的方案設計。一些極端的秒殺場景帶來的熱key問題,如果我們預算充足,可以直接做服務的業務隔離、redis緩存集群的隔離,避免影響到正常業務的同時,也會可以臨時採取更好的容災、限流措施。
目前市面上已經有了不少關於hotKey相對完整的應用級解決方案,其中京東在這方面有開源的hotkey工具,原理就是在client端做洞察,然後上報對應hotkey,server端偵測到後,將對應hotkey下發到對應服務端做本地緩存,並且這個本地緩存在遠端對應的key更新後,會同步更新,已經是目前較成熟的自動探測熱key、分散式一致性快取
解決方案,京東零售熱key。
以上就是筆者大概了解或實踐過的的如何應對熱key的一些方案,從發現熱key到解決熱key的兩個關鍵問題的應對。每一個方案都有優缺點,例如會帶來業務的不一致性,實施起來較為困難等等,可以根據目前自身業務的特徵、以及目前公司的基建去做對應的調整和改變。
更多程式相關知識,請造訪:程式設計入門! !
以上是聊聊Redis中如何應對快取熱key問題?常用方案分享的詳細內容。更多資訊請關注PHP中文網其他相關文章!