全面解析快取應用經典問題

JIAN2發表於2023-03-13

1、前言

隨著網際網路從簡單的單向瀏覽請求,發展為基於使用者個性資訊的定製化以及社交化的請求,這要求產品需要做到以使用者和關係為基礎,對海量資料進行分析和計算。對於後端服務來說,意味著使用者的每次請求都需要查詢使用者的個人資訊和大量的關係資訊,此外大部分場景還需要對上述資訊進行聚合、過濾、排序,最終才能返回給使用者。

CPU 是資訊處理、程式執行的最終執行單元,如果它的世界也有 “秒” 的概念,假設它的時鐘跳一下為一秒,那麼在 CPU(CPU 的一個核心)眼中的時間概念是什麼樣的呢?



可見 I/O 的速度與 CPU 和記憶體相比是要差幾個數量級的,如果資料全部從資料庫獲取,一次請求涉及多次資料庫操作會大大增加響應時間,無法提供好的使用者體驗。

對於大型高併發場景下的 Web 應用,快取更為重要,更高的快取命中率就意味著更好的效能。快取系統的引入,是提升系統響應時延、提升使用者體驗的唯 一途徑,良好的快取架構設計也是高併發系統的基石。


快取的思想基於以下幾點:

  • 時間侷限性原理 程式有在一段時間內多次訪問同一個資料塊的傾向。例如一個熱門的商品或者一個熱門的新聞會被數以百萬甚至千萬的更多使用者檢視。透過快取,可以高效地重用之前檢索或計算的資料。

  • 以空間換取時間 對於大部分系統,全量資料通常儲存在 MySQL 或者 Hbase,但是它們的訪問效率太低。所以會開闢一個高速的訪問空間來加速訪問過程,例如 Redis 讀的速度是 110000 次 /s,寫的速度是 81000 次 /s 。

  • 效能和成本的 Tradeoff 高速的訪問空間帶來的是成本的提升,在系統設計時要兼顧效能和成本。例如,在相同成本的情況下,SSD 硬碟容量會比記憶體大 10~30 倍以上,但讀寫延遲卻高 50~100 倍。


引入快取會給系統帶來以下優勢:

  • 提升請求效能

  • 降低網路擁塞

  • 減輕服務負載

  • 增強可擴充套件性


同樣的,引入快取也會帶來以下劣勢:

  • 毫無疑問會增加系統的複雜性,開發複雜性和運維複雜性成倍提升。

  • 高速的訪問空間會比資料庫儲存的成本高。

  • 由於一份資料同時存在快取和資料庫中,甚至快取內部也會有多個資料副本,多份資料就會存在資料雙寫的不一致問題,同時快取體系本身也會存在可用性問題和分割槽的問題。




在快取系統的設計架構中,還有很多坑,很多的明槍暗箭,如果設計不當會導致很多嚴重的後果。設計不當,輕則請求變慢、效能降低,重則會資料不一致、系統可用性降低,甚至會導致快取雪崩,整個系統無法對外提供服務。


2、快取的主要儲存模式

三種模式各有優劣,適用於不同的業務場景,不存在最 佳模式。

● Cache Aside(旁路快取)

寫: 更新 db 時,刪除快取,當下次讀取資料庫時,驅動快取的更新。

讀: 讀的時候先讀快取,快取未命中,那麼就讀資料庫,並且將資料回種到快取,同時返回相應結果

特點:懶載入思想,以資料庫中的資料為準。在稍微複雜點的快取場景,快取都不簡單是資料庫中直接取出來的,可能還需要從其他表查詢一些資料,然後進行一些複雜的運算,才能最終計算出值。這種儲存模式適合於對資料一致性要求比較高的業務,或者是快取資料更新比較複雜、代價比較高的業務。例如:一個快取涉及多個表的多個欄位,在 1 分鐘內被修改了 100 次,但是這個快取在 1 分鐘內就被讀取了 1 次。如果使用這種儲存模式只刪除快取的話,那麼 1 分鐘內,這個快取不過就重新計算一次而已,開銷大幅度降低。



● Read/Write Through(讀寫穿透)

寫: 快取存在,更新資料庫,快取不存在,同時更新快取和資料庫

讀: 快取未命中,由快取服務載入資料並且寫入快取

特點:

讀寫穿透對熱資料友好,特別適合有冷熱資料區分的場合。

1)簡化應用程式程式碼

在快取方法中,應用程式程式碼仍然很複雜,並且直接依賴於資料庫,如果多個應用程式處理相同的資料,甚至會出現程式碼重複。讀寫穿透模式將一些資料訪問程式碼從應用程式轉移到快取層,這極大地簡化了應用程式並更清晰地抽象了資料庫操作。

2)具有更好的讀取可伸縮性

在多數情況下,快取資料過期以後,多個並行使用者執行緒最終會打到資料庫,再加上數以百萬計的快取項和數千個並行使用者請求,資料庫上的負載會顯著增加。讀寫穿透可以保證應用程式永遠不會為這些快取項訪問資料庫,這也可以讓資料庫負載保持在最小值。

3)具有更好的寫效能

讀寫穿透模式可以讓應用程式快速更新快取並返回,之後它讓快取服務在後臺更新資料庫。當資料庫寫操作的執行速度不如快取更新的速度快時,還可以指定限流機制,將資料庫寫操作安排在非高峰時間進行,減輕資料庫的壓力。

4)過期時自動重新整理快取

讀寫穿透模式允許快取在過期時自動從資料庫重新載入物件。這意味著應用程式不必在高峰時間訪問資料庫,因為最新資料總是在快取中。



● Write Behind Caching(非同步快取寫入)

寫:只更新快取,快取服務非同步更新資料庫。

讀:快取未命中由封裝好的快取服務載入資料並且寫入快取。

特點:寫效能最高,定期非同步重新整理資料庫資料,資料丟失的機率大,適合寫頻率高,並且寫操作需要合併的場景。使用非同步快取寫入模式,資料的讀取和更新透過快取進行,與讀寫穿透模式不同,更新的資料並不會立即傳到資料庫。相反,在快取服務中一旦進行更新操作,快取服務就會跟蹤髒記錄列表,並定期將當前的髒記錄集重新整理到資料庫中。作為額外的效能改善,快取服務會合並這些髒記錄,合併意味著如果相同的記錄被更新,或者在緩衝區內被多次標記為髒資料,則只保證最後一次更新。對於那些值更新非常頻繁,例如金融市場中的股票價格等場景,這種方式能夠很大程度上改善效能。如果股票價格每秒鐘變化 100 次,則意味著在 30 秒內會發生 30 x 100 次更新,合併將其減少至只有一次。



3、快取 7 大經典問題

問題的常用解決方案

  1   快取集中失效

快取集中失效大多數情況出現在高併發的時候,如果大量的快取資料集中在一個時間段失效,查詢請求會打到資料庫,資料庫壓力凸顯。比如同一批火車票、飛機票,當可以售賣時,系統會一次性載入到快取,並且過期時間設定為預先配置的固定時間,那過期時間到期後,系統就會因為熱點資料的集中沒有命中而出現效能變慢的情況。

解決方案:

  • 使用基準時間 + 隨機時間,降低過期時間的重複率,避免集體失效。即相同業務資料設定快取失效時間時,在原來設定的失效時間基礎上,再加上一個隨機值,讓資料分散過期,同時對資料庫的請求也會分散開,避免瞬時全部過期對資料庫造成過大壓力。

  2   快取穿透

快取穿透是指一些異常訪問,每次都去查詢壓根兒就不存在的 key,導致每次請求都會打到資料庫上去。例如查詢不存在的使用者,查詢不存在的商品 id。如果是使用者偶爾錯誤輸入,問題不大。但如果是一些特殊使用者,控制一批肉雞,持續的訪問快取不存在的 key,會嚴重影響系統的效能,影響正常使用者的訪問,甚至可能會讓資料庫直接當機。我們在設計系統時,通常只考慮正常的訪問請求,所以這種情況往往容易被忽略。

解決方案:

  • 第一種方案就是,查詢到不存在的資料時,首 次查詢資料庫,即便資料庫沒有資料,仍然回種這個 key 到快取,並使用一個特殊約定的 value 表示這個 key 的值為空。後面再次出現對這個 key 的請求時,直接返回 null。為了健壯性,設定空快取 key 時,一定要設定過期時間,以防止之後該 key 被寫入了資料。

  • 第二種方案是,構建一個 BloomFilter 快取過濾器,記錄全量資料,這樣訪問資料時,可以直接透過 BloomFilter 判斷這個 key 是否存在,如果不存在直接返回即可,壓根兒不需要查詢快取或資料庫。比如,可以使用基於資料庫增量日誌解析框架(阿里的 canal),透過消費增量資料寫入到 BloomFilter 過濾器。BloomFilter 的所有操作也是在記憶體裡實現,效能很高,要達到 1% 的誤判率,平均單條記錄佔用 1.2 位元組即可。同時需要注意的是 BloomFilter 只有新增沒有刪除操作,對於已經刪除的 key 可以配合上述快取空值解決方案一起使用。Redis 提供了自定義引數的布隆顧慮器,可以使用 bf.reserve 進行建立,需要設定引數 error_rate(錯誤率)和 innitial_size。error_rate 越低需要的空間越大,innitial_size 表示預計放入的元素數量,當實際數量超過這個值以後,誤判率會上升。


  3   快取雪崩

快取雪崩是快取機器因為某種原因全部或者部分當機,導致大量的資料落到資料庫,最終把資料庫打死。例如某個服務,恰好在請求高峰期間快取服務當機,本來打到快取的請求,這是時候全部打到資料庫,資料庫扛不住在報警以後也會當機,重啟資料庫以後,新的請求會再次把資料庫打死。

解決方案:

  • 事前:快取採用高可用架構設計,redis 使用叢集部署方式。對重要業務資料的資料庫訪問新增開關,當發現資料庫出現阻塞、響應慢超過閾值的時候,關閉開關,將一部分或者全都的資料庫請求執行 failfast 操作。

  • 事中:引入多級快取架構,增加快取副本,比如新增本地 ehcache 快取。引入限流降級元件,對快取進行實時監控和實時報警。透過機器替換、服務替換進行及時恢復;也可以透過各種自動故障轉移策略,自動關閉異常介面、停止邊緣服務、停止部分非核心功能措施,確保在極端場景下,核心功能的正常執行。

  • 事後:redis 持久化,支援同時開啟兩種持久化方式,我們可以綜合使用 AOF 和 RDB 兩種持久化機制,用 AOF 來保證資料不丟失,作為資料恢復的第一選擇;用 RDB 來做不同程度的冷備,在 AOF 檔案都丟失或損壞不可用的時候,還可以使用 RDB 來進行快速的資料恢復。同時把 RDB 資料備份到遠端的雲服務,如果伺服器記憶體和磁碟的資料同時丟失,依然可以從遠端拉取資料做災備恢復操作。


  4   快取資料不一致

同一份資料,既在快取裡又在資料庫裡,肯定會出現資料庫與快取中的資料不一致現象。如果引入多級快取架構,快取會存在多個副本,多個副本之間也會出現快取不一致現象。快取機器的頻寬被打滿,或者機房網路出現波動時,快取更新失敗,新資料沒有寫入快取,就會導致快取和 DB 的資料不一致。快取 rehash 時,某個快取機器反覆異常,多次上下線,更新請求多次 rehash。這樣,一份資料存在多個節點,且每次 rehash 只更新某個節點,導致一些快取節點產生髒資料。再比如,資料發生了變更,先刪除了快取,然後要去修改資料庫,此時還沒修改。一個請求過來,去讀快取,發現快取空了,去查詢資料庫,查到了修改前的舊資料,放到了快取中。隨後資料變更的程式完成了資料庫的修改,資料庫和快取中的資料不一樣了。

解決方案:

  • 設定 key 的過期時間儘量短,讓快取更早的過期,從 db 載入新資料,這樣無法保證資料的強一致性,但是可以保證最終一致性。

    cache 更新失敗以後引入重試機制,比如連續重試失敗以後,可以將操作寫入重試佇列,當快取服務可用時,將這些 key 從快取中刪除,當這些 key 被重新查詢時,重新從資料庫回種。

    延時雙刪除策略,首先刪除快取中的資料,在寫資料庫,休眠一秒以後(具體時間需要根據具體業務邏輯的耗時進行調整)再次刪除快取。這樣可以將一秒內造成的所有髒資料再次刪除。

    快取最終一致性,使客戶端資料與快取解耦,應用直接寫資料到資料庫中。資料庫更新 binlog 日誌,利用 Canal 中介軟體讀取 binlog 日誌。Canal 藉助於限流元件按頻率將資料發到 MQ 中,應用監控 MQ 通道,將 MQ 的資料更新到 Redis 快取中。

    更新資料的時候,根據資料的唯 一標識,將操作路由之後,傳送到一個 jvm 內部佇列中。讀取資料的時候,如果發現資料不在快取中,那麼將重新執行 “讀取資料 + 更新快取” 的操作,根據唯 一標識路由之後,也傳送到同一個 jvm 內部佇列中。該方案對於讀請求進行了非常輕度的非同步化,使用一定要注意讀超時的問題,每個讀請求必須在超時時間範圍內返回。因此需要根據自己的業務情況進行測試,可能需要部署多個服務,每個服務分攤一些資料的更新操作。如果一個記憶體佇列里居然會擠壓 100 個業務資料的修改操作,每個操作操作要耗費 10ms 去完成,那麼最後一個讀請求,可能等待 10 * 100 = 1000ms = 1s 後,才能得到資料,這個時候就導致讀請求的長時阻塞。


  5   競爭併發

當系統的線上流量特別大的時候,快取中會出現資料併發競爭的現象。在高併發的場景下,如果快取資料正好過期,各個併發請求之間又沒有任何協調動作,這樣併發請求就會打到資料庫,對資料造成較大的壓力,嚴重的可能會導致快取 “雪崩”。另外高併發競爭也會導致資料不一致問題,例如多個 redis 客戶端同時 set 同一個 key 時,key 最開始的值是 1,本來按順序修改為 2,3,4,最後是 4,但是順序變成了 4,3,2,最後變成了 2。

解決方案:

分散式鎖 + 時間戳

可以基於 redis 或者 zookeeper 實現一個分散式鎖,當一個 key 被高併發訪問時,讓請求去搶鎖。也可以引入訊息中介軟體,把 Redis.set 操作放在訊息佇列中。總之,將並行讀寫改成序列讀寫的方式,從而來避免資源競爭。對於 key 的操作的順序性問題,可以透過設定一個時間戳來解決。大部分場景下,要寫入快取的資料都是從資料庫中查詢出來的。在資料寫入資料庫時,可以維護一個時間戳欄位,這樣資料被查詢出來時都會帶一個時間戳。寫快取的時候,可以判斷一下當前資料的時間戳是否比快取裡的資料的時間戳要新,這樣就避免了舊資料對新資料的覆蓋。


  6   熱點 Key 問題

對於大多數網際網路系統,資料是分冷熱的,訪問頻率高的 key 被稱為熱 key,比如熱點新聞、熱點的評論。而在突發事件發生時,瞬間會有大量使用者去訪問這個突發熱點資訊,這個突發熱點資訊所在的快取節點由於超大流量而達到理網路卡、頻寬、CPU 的極限,從而導致快取訪問變慢、卡頓、甚至當機。接下來資料請求到資料庫,最終導致整個服務不可用。比如微博中數十萬、數百萬的使用者同時去吃一個新瓜,秒殺、雙 11、618 、春節等線上促銷活動,明星結婚、離婚、出軌這種特殊突發事件。

解決方案:

要解決這種極熱 key 的問題,首先要找出這些 熱 key 。對於重要節假日、線上促銷活動、憑藉經驗可以提前評估出可能的熱 key 來。而對於突發事件,無法提前評估,可以透過 Spark 或者 Flink,進行流式計算,及時發現新發布的熱點 key。而對於之前已發出的事情,逐步發酵成為熱 key 的,則可以透過 Hadoop 進行離線跑批計算,找出最近歷史資料中的高頻熱 key。還可以透過客戶端進行統計或者上報。找到熱 key 後,就有很多解決辦法了。首先可以將這些熱 key 進行分散處理。redis cluster 有固定的 16384 個 hash slot,對每個 key 計算 CRC16 值,然後對 16384 取模,可以獲取 key 對應的 hash slot。比如一個熱 key 名字叫 hotkey,可以被分散為 hotkey#1、hotkey#2、hotkey#3,……hotkey#n,這 n 個 key 就會分散存在多個快取節點,然後 client 端請求時,隨機訪問其中某個字尾的熱 key,這樣就可以把熱 key 的請求打散。


其次,也可以 key 的名字不變,對快取提前進行多副本 + 多級結合的快取架構設計。比如利用 ehcache,或者一個 HashMap 都可以。在你發現熱 key 以後,把熱 key 載入到系統的 JVM 中,之後針對熱 key 的請求,可以直接從 jvm 中獲取資料。再次,如果熱 key 較多,還可以透過監控體系對快取的 SLA 實時監控,透過快速擴容來減少熱 key 的衝擊。


  7   大 Key 問題

有些時候開發人員設計不合理,在快取中會形成特別大的物件,這些大物件會導致資料遷移卡頓,另外在記憶體分配 方面,如果一個 key 特別大,當需要擴容時,會一次性申請更大的一塊記憶體,這也會導致卡頓。如果大物件被刪除,記憶體會被一次性回收,卡頓現象會再次發生。在平時的業務開發中,要儘量避免大 key 的產生。如果發現系統的快取大起大落,極有可能是大 key 引起的,這就需要開發人員定位出大 key 的來源,然後進行相關的業務程式碼重構。Redis 官方已經提供了相關的指令進行大 key 的掃描,可以直接使用。

解決方案: 

  • 如果資料存在 Redis 中,比如業務資料存 set 格式,大 key 對應的 set 結構有幾千幾萬個元素,這種寫入 Redis 時會消耗很長的時間,導致 Redis 卡頓。此時,可以擴充套件新的資料結構,同時讓 client 在這些大 key 寫快取之前,進行序列化構建,然後透過 restore 一次性寫入。

  • 將大 key 分拆為多個 key,儘量減少大 key 的存在。同時由於大 key 一旦穿透到 DB,載入耗時很大,所以可以對這些大 key 進行特殊照顧,比如設定較長的過期時間,比如快取內部在淘汰 key 時,同等條件下,儘量不淘汰這些大 key。





來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70006413/viewspace-2939421/,如需轉載,請註明出處,否則將追究法律責任。

相關文章