天天看點

聊一聊安全且正确使用緩存的那些事——關于可靠性、關乎一緻性

作者:架構悟道
聊一聊安全且正确使用緩存的那些事——關于可靠性、關乎一緻性

大家好,又見面了。

在上一篇文檔《聊一聊作為高并發系統基石之一的緩存,會用很簡單,用好才是技術活》中,我們對緩存的龐大體系進行了個初步的探讨,浮光掠影般的介紹了本地緩存、集中緩存、多級緩存的不同形式,也走馬觀花似的初識了緩存設計的關鍵原則與需要關注的典型問題。

作為《深入了解緩存原理與實戰設計》系列專欄的第二篇内容,從本篇開始,我們将聚焦緩存體系中的具體場景,分别進行深入的闡述與探讨。本篇我們就一起具體地聊一聊緩存使用中需要關注的典型問題與可靠性防護措施。

在分布式系統盛行的今天,尤其是在一些使用者體量比較大的網際網路業務系統裡面,緩存充當着扛壓屏障的作用。目前各網際網路系統可以扛住動辄數萬甚至數十萬的并發請求量,緩存機制功不可沒。而一旦緩存出現問題,對系統的影響往往也是緻命的。是以在緩存的使用時必須要考慮完備的兜底與災難應對政策。

聊一聊安全且正确使用緩存的那些事——關于可靠性、關乎一緻性

熱點資料與淘汰政策

大部分服務端使用的抗壓型緩存,為了保證緩存執行速度,普遍都是将資料存儲在記憶體中。而受限于硬體與成本限制,記憶體的容量不太可能像磁盤一樣近乎無限地去随意擴容使用。對于實際資料量極其龐大且無法将其全部存儲于緩存中的時候,我們需要保證存儲在緩存中的有限部分資料要盡可能的命中更多的請求,即要求緩存中存儲的都是熱點資料。

說到這裡,就會存在一個不得不面對的問題:當資料量超級大而緩存的記憶體容量有限的情況下,如果容量滿了該怎麼辦?

斷舍離!

緩存實作的時候,必須要有一種機制,能夠保證記憶體中的資料不會無限制增加 —— 也即資料淘汰機制。資料淘汰機制,是一個成熟的緩存體系所必備的基礎能力。這裡有個概念需要厘清,即資料淘汰政策與資料過期是兩個不同的概念。

  • 資料過期,是緩存系統的一個正常邏輯,是符合業務預期的一種資料删除機制。即設定了有效期的緩存資料,過期之後從緩存中移除。
  • 資料淘汰,是緩存系統的一種“有損自保”的降級政策,是業務預期之外的一種資料删除手段。指的是所存儲的資料沒達到過期時間,但緩存空間滿了,對于新的資料想要加入緩存中時,緩存子產品需要執行的一種應對政策。
聊一聊安全且正确使用緩存的那些事——關于可靠性、關乎一緻性

我們把緩存當做一個容器,試想一下,一個容器已滿的情況下,繼續往裡面放東西,可以有什麼應對之法?無外乎兩種:

  1. 直接拒絕,因為滿了,放不下了。
  2. 從容器裡面扔掉一些已有内容,然後騰挪出部分空間出來,将新的東西放進來。
聊一聊安全且正确使用緩存的那些事——關于可靠性、關乎一緻性

進一步地,當決定采用先從容器中扔掉一些已有内容的時候,又會面臨一個新的抉擇,應該扔掉哪些内容?實踐中常用的也有幾種方案:

  1. 一切随緣,随機決定。從容器中現有的内容中随機扔掉剔除一些。
  2. 按需排序,保留常用。即基于LRU政策,将最久沒有被使用過的資料給剔除掉。
  3. 提前過期,淘汰出局。對于一些設定了過期時間的記錄,将其按照過期時間點進行排序,将最近即将過期的資料剔除(類似讓其提前過期)。
  4. 其它政策。自行實作緩存時,除了上述集中常見政策,也可以根據業務的場景建構業務自定義的淘汰政策。比如根據建立日期、根據最後修改日期、根據優先級、根據通路次數等等。

一些主流的緩存中間件的淘汰機制大都也是遵循上述的方案來實作的。比如Redis提供了高達6種不同的資料淘汰機制,供使用方按需選擇,将有限的空間僅用來存儲熱點資料,實作緩存的價值最大化。如下:

聊一聊安全且正确使用緩存的那些事——關于可靠性、關乎一緻性

從上圖可以看出,Redis對随機淘汰和LRU政策進行的更精細化的實作,支援将淘汰目标範圍細分為全部資料和設有過期時間的資料,這種政策相對更為合理一些。因為一般設定了過期時間的資料,本身就具備可删除性,将其直接淘汰對業務不會有邏輯上的影響;而沒有設定過期時間的資料,通常是要求常駐記憶體的,往往是一些配置資料或者是一些需要當做白名單含義使用的資料(比如使用者資訊,如果使用者資訊不在緩存裡,則說明使用者不存在),這種如果強行将其删除,可能會造成業務層面的一些邏輯異常。

聊一聊安全且正确使用緩存的那些事——關于可靠性、關乎一緻性

緩存雪崩:避免緩存的集中失效

為了限制緩存的數量,很多的緩存記錄都會設定一定的有效期,到期後自動失效。這種在一些批量緩存建構或者全量緩存重建時,因為設定了相同的失效時間,會導緻大量甚至全部的緩存資料在短時間内集體失效,這樣會導緻大量的請求無法命中緩存而直接流轉到了下遊子產品,導緻系統癱瘓,也即緩存雪崩。

聊一聊安全且正确使用緩存的那些事——關于可靠性、關乎一緻性

其實解決的思路也很簡單,避免出現集中失效就好咯。如何避免呢?

一種簡單的政策,就是批量加載的場景,将過期時間在一個固定時間段内以毫秒級别進行随機打散,比如本來要設定每條記錄過期時間為5分鐘,則批量加載的時候可以設定過期時間為5~10分鐘之間的任意一個毫秒數。這樣就可以有效地避免資料集中失效,避免出現緩存雪崩而影響業務穩定。

此外,在一些大型系統裡面,尤其是一些分布式微服務化的系統中,很多情況下都會有多個獨立的緩存服務,而最終持久化資料則集中存儲。如果某個獨立緩存真的出現了緩存雪崩,業務層面應該如何将受損範圍控制在僅自身子產品、避免殃及資料庫以及下遊公共服務子產品,進而避免業務出現系統性癱瘓呢?這個就需要結合服務治理中的一些手段來綜合防範了,比如服務降級、服務熔斷、以及接口限流等政策。

聊一聊安全且正确使用緩存的那些事——關于可靠性、關乎一緻性
聊一聊安全且正确使用緩存的那些事——關于可靠性、關乎一緻性

緩存擊穿:有效的冷資料預熱加載機制

正如前面所提到的,基于記憶體的緩存,受記憶體容量限制,往往都會加載一些熱點資料。而這些熱點緩存資料,可以命中大部分的業務請求。少部分沒有命中緩存的資料,則直接轉由業務子產品進行處理(比如從MySQL裡面進行查詢)。

先來看一個例子:

互動論壇系統,使用Redis作為緩存,緩存最近1年的文章資訊。如果使用者檢視的文章是最近1年的,則直接從Redis中查詢并傳回,如果使用者檢視的文章是1年前的,則從MySQL中進行撈取并傳回。

因為論壇系統中,大部分人會閱讀或者檢視的都是最近新發的文章,隻有極少數的人可能會偶爾“挖墳”檢視一年前的曆史文章。系統上線前會根據冷熱請求的比例與總量情況,評估需要部署的硬體規模,以確定可以支撐住線上正常的通路請求。但為了避免緩存資料被無限撐滿,一般業務緩存資料都會設定一個過期時間,來保證緩存資料的定期清理與更新。

近段時間,娛樂圈的雷聲不斷,各種新鮮的大瓜也讓吃瓜群衆撐到打嗝。

有一天,娛樂圈當紅流量明星李某某突然被爆料與某網紅存在某些不正當的關系,甚至被爆有多次PC被捕的驚天大瓜,引起粉絲和路人的強烈關注。

吃瓜群衆們群情高漲、熱搜一波蓋過一波、文章的浏覽量光速攀升,論壇系統在緩存子產品的加持下,雖然整體CPU和記憶體占用都飙升上去了,倒也相安無事。

但天有不測風雲,恰好這個時候,這條文章的記錄在緩存中過期被删除了。然後狂濤巨浪般的請求湧向了後端的資料庫,讓資料庫原地癱瘓,進而陸陸續續殃及了整個論壇系統。這就是典型的一個緩存擊穿的問題。

聊一聊安全且正确使用緩存的那些事——關于可靠性、關乎一緻性

緩存擊穿和前面提到的緩存雪崩産生的原因其實很相似。差別點在于:

  • 緩存雪崩是大面積的緩存失效導緻大量請求湧入資料庫。
  • 緩存擊穿是少量緩存失效的時候恰好失效的資料遭遇大并發量的請求,導緻這些請求全部湧入資料庫中。

針對這種情況,我們可以為熱點資料設定一個過期時間續期的操作,比如每次請求的時候自動将過期時間續期一下。此外,也可以在資料庫記錄通路的時候借助分布式鎖來防止緩存擊穿問題的出現。當緩存不可用時,僅持鎖的線程負責從資料庫中查詢資料并寫入緩存中,其餘請求重試時先嘗試從緩存中擷取資料,避免所有的并發請求全部同時打到資料庫上。如下圖所示:

聊一聊安全且正确使用緩存的那些事——關于可靠性、關乎一緻性

對上面的處理過程描述說明如下:

  1. 沒有命中緩存的時候,先請求擷取分布式鎖,擷取到分布式鎖的線程,執行DB查詢操作,然後将查詢結果寫入到緩存中;
  2. 沒有搶到分布式鎖的請求,原地自旋等待一定時間後進行再次重試;
  3. 未搶到鎖的線程,再次重試的時候,先嘗試去緩存中擷取下是否能擷取到資料,如果可以擷取到資料,則直接取緩存已有的資料并傳回;否則重複上述1、2、3步驟。

按照上面的政策,經過一番通宵緊急上線操作後,系統終于恢複了正常。正當開發人員長舒了口氣準備下班回家睡覺的時候,系統警報再次響起,系統再次當機了。

有人扒出了一個2年前的文章,這個文章在2年前就已經爆料李某某由于PC被警方拘捕,當時大家都不信。于是這個2年前的文章得到了衆人狂熱的轉發與閱讀檢視。

其實當機的原因很明顯,因為系統隻規劃緩存了最近1年的所有文章資訊,而對超過1年的文章的操作,都會直接請求到資料庫上。這個2年前的文章突然爆火導緻大量的使用者來請求直接打到了下遊,再次将資料庫壓垮 —— 也就是說又一次出現了緩存擊穿,在同一塊石頭上摔倒了兩次!

聊一聊安全且正确使用緩存的那些事——關于可靠性、關乎一緻性

對于業務中最常使用的旁路型緩存而言,通常會先讀取緩存,如果不存在則去資料庫查詢,并将查詢到的資料添加到緩存中,這樣就可以使得後面的請求繼續命中緩存。

聊一聊安全且正确使用緩存的那些事——關于可靠性、關乎一緻性

但是這種正常操作存在個“漏洞”,因為大部分緩存容量有限制,且很多場景會基于LRU政策進行記憶體中熱點資料的淘汰,假如有個惡意程式(比如爬蟲)一直在刷曆史資料,容易将記憶體中的熱點資料變為曆史資料,導緻真正的使用者請求被打到資料庫層。因而又出現了一些業務場景,會使用類似上面所舉的例子的政策,緩存指定時間段内的資料(比如最近1年),且資料不存在時從DB擷取内容之後也不會回寫到緩存中。針對這種場景,在緩存的設計時,需要考慮到對這種冷資料的加熱機制進行一些額外處理,如設定一個門檻,如果指定時間段内對一個冷資料的通路次數達到門檻值,則将冷資料加熱,添加到熱點資料緩存中,并設定一個獨立的過期時間,來解決此類問題。

比如上面的例子中,我們可以約定同一秒内對某條冷資料的請求超過10次,則将此條冷資料加熱作為臨時熱點資料存入緩存,設定緩存過期時間為30天(一般一個陳年八卦一個月足夠消停下去了)。通過這樣的機制,來解決冷資料的突然竄熱對系統帶來的不穩定影響。如下圖所示:

聊一聊安全且正确使用緩存的那些事——關于可靠性、關乎一緻性

又是一番緊急上線,終于,系統又恢複正常了。

聊一聊安全且正确使用緩存的那些事——關于可靠性、關乎一緻性

緩存穿透:合理的防身自保手段

我們的系統對外開放并運作的時候,面對的環境險象環生。你不知道請求是來自一個正常使用者還是某些别有用心的盜竊者、亦或是個純粹的破壞者。

還是上面的論壇的例子:

使用者在互動論壇上點選文章并檢視内容的時候,界面調用查詢文章詳情接口時會傳入文章ID,然後後端基于文章ID先去緩存中查詢,如果緩存中存在則直接傳回資料,否則會嘗試從MySQL中查詢資料并傳回。

有些人盯上了論壇的内容,便搞了個爬蟲程式,模拟文章ID的生成規則,調用查詢詳情接口并傳入自己生成的ID去周遊挖取系統内的文章資料,這樣導緻很多傳入的ID是無效的、系統内并不存在對應ID的文章資料。

聊一聊安全且正确使用緩存的那些事——關于可靠性、關乎一緻性

是以,上面大量無效的ID請求到系統内,因為無法命中緩存而被轉到MySQL中查詢,而MySQL中其實也無法查詢到對應的資料(因為這些ID是惡意生成的、壓根不存在)。大量此類請求頻繁的傳入,就會導緻請求一直依賴MySQL進行處理,極易沖垮下遊子產品。這個便是經典的緩存穿透問題(緩存穿透與緩存擊穿非常相似,差別點在于緩存穿透的實際請求資料在資料庫中也沒有,而緩存擊穿是僅僅在緩存中沒命中,但是在資料庫中其實是存在對應資料的)。

緩存穿透的情況往往出現在一些外部幹擾或者攻擊情景中,比如外部爬蟲、比如黑客攻擊等等。為了解決緩存穿透的問題,可以考慮基于一些類似白名單的機制(比如基于布隆過濾器的政策,後面系列文章中會詳細探讨),當然,有條件的情況下,也可以建構一些反爬政策,比如添加請求簽名校驗機制、比如添加IP通路限制政策等等。

聊一聊安全且正确使用緩存的那些事——關于可靠性、關乎一緻性

緩存的資料一緻性

緩存作為持久化存儲(如資料庫)的輔助存在,畢竟屬于兩套系統。理想情況下是緩存資料與資料庫中資料完全一緻,但是業務最常使用的旁路緩存架構下,在一些分布式或者高并發的場景中,可能會出現緩存不一緻的情況。

資料庫更新+緩存更新

在資料有變更的時候,需要同時更新緩存和資料庫兩個地方的資料。因為涉及到兩個子產品的資料更新,是以會有2種組合情況:

  • 先更新緩存,再更新資料庫
  • 先更新資料庫, 再更新緩存
聊一聊安全且正确使用緩存的那些事——關于可靠性、關乎一緻性

在單線程場景下,如果更新緩存和更新資料庫操作都是成功的,則可以保證資料庫與緩存資料是一緻的。但是在多線程場景下,由于由于更新緩存和更新資料庫是兩個操作,不具備原子性,就有可能出現多個并發請求交叉的情況,進而導緻緩存和資料庫中的記錄不一緻的情況。比如下面這個場景:

聊一聊安全且正确使用緩存的那些事——關于可靠性、關乎一緻性

這種情況下,有很多的人會選擇結合資料庫的事務來一起控制。因為資料庫有事務控制,而Redis等緩存沒有事務性,是以會在一個DB事務中封裝多個操作,比如先執行資料庫操作,執行成功之後再進行緩存更新操作。這樣如果緩存更新失敗,則直接将目前資料庫的事務復原,企圖用這種方式來保證緩存資料與DB資料的一緻。

聊一聊安全且正确使用緩存的那些事——關于可靠性、關乎一緻性

乍看似乎沒毛病,但是細想一下,其實是有前提條件的。我們知道資料庫事務的隔離級别有幾種不同的類型,需要保證使用的事務隔離級别為Serializable或者Repeatable Read級别,以此來保證并發更新的場景下不會出現資料不一緻問題,但這也降低了并發效率,提高資料庫的CPU負載(隔離級别與并發性能存在一定的關聯關系,見下圖所示)。

聊一聊安全且正确使用緩存的那些事——關于可靠性、關乎一緻性

是以對于一些讀多寫少、寫操作并發競争不是特别激烈且對一緻性要求不是特别高的情況下,可以采用事務(高隔離級别) + 先更新資料庫再更新緩存的方式來達到資料一緻的訴求。

聊一聊安全且正确使用緩存的那些事——關于可靠性、關乎一緻性

資料庫更新+緩存删除

在旁路型緩存的讀操作分支中,從緩存中沒有讀取到資料而改為從DB中擷取到資料之後,通常都會選擇将記錄寫入到緩存中。是以我們也可以在寫操作的時候選擇将緩存直接删除,等待後續讀取的時候重新加載到緩存中。

這樣也會有兩種組合情況:

  • 先删除緩存,再更新資料庫
  • 先更新資料庫,再删除緩存
聊一聊安全且正确使用緩存的那些事——關于可靠性、關乎一緻性

這種也會出現前面說的先操作成功,後操作失敗的問題。

我們先看下先删除緩存再更新資料庫的操作政策。如果先删除緩存成功,然後更新資料庫失敗,這種情況下,再次讀取的時候,會從DB裡面将舊資料重新加載回緩存中,資料是可以保持一緻的。

雖然更新資料庫失敗這種場景下不會出現問題,但是在資料庫更新成功這種正常情況下,卻可能會在并發場景中出現問題。因為常見的緩存(如Redis)是沒有事務的,是以可能會因為并發處理順序的問題導緻最終資料不一緻。如下圖所示:

聊一聊安全且正确使用緩存的那些事——關于可靠性、關乎一緻性

上圖中,因為删除緩存和更新DB是非原子操作,是以在并發場景下可能的情況:

  1. A請求執行更新資料操作,先删除了緩存中的資料;
  2. A這個時候還沒來及往DB中更新資料的時候,B查詢請求恰好進入;
  3. B先查詢緩存發現緩存中沒有資料,又從資料庫中查詢記錄并将記錄寫入緩存中(相當于A剛删了緩存,B又将原樣資料寫回緩存了);
  4. A執行完成更新邏輯,将變更後的資料寫入到DB中。

一番操作完成後,實際上緩存中存儲的是A修改前的内容,而DB中存儲的是A修改後的資料,兩者是以出現了不一緻的問題。這樣導緻後面的查詢請求依舊是從緩存中擷取到舊資料,而更新後的新資料無法生效。

那麼,如果采用先更新資料庫,再删除緩存的政策,又會有何種表現呢?假設資料庫更新成功,但是緩存删除失敗,我們也可以通過資料庫事務復原的方式将資料庫更新操作復原掉,這樣在非并發狀态下,可以確定資料庫與緩存中資料是一緻的。

聊一聊安全且正确使用緩存的那些事——關于可靠性、關乎一緻性

當然,因為基于資料庫事務機制來控制,需要注意下事務的粒度不能過大,避免事務成為阻塞系統性能的瓶頸。在對并發性能要求極高的情況下,可以考慮非事物類的其餘方式來實作,如重試機制、或異步補償機制、或多者結合方式等。

比如下圖所示的這種政策:

聊一聊安全且正确使用緩存的那些事——關于可靠性、關乎一緻性

上圖的資料更新處理政策,可以有效的保證資料的最終一緻性,降低極端情況可能出現資料不一緻的機率,并兜底增加了資料不一緻時的自恢複能力。

具體處理邏輯說明如下:

  • 先執行資料庫的資料更新操作。
  • 更新成功,再去執行緩存記錄删除操作。
  • 緩存如果删除失敗,則按照預定的重試政策(比如對于指定錯誤碼進行重試,最多重試3次,每次重試間隔100ms等)進行重試。
  • 如果緩存删除失敗,且重試依舊失敗,則将此删除事件放入到MQ中。
  • 獨立的補償邏輯,會去消費MQ中的消息事件請求,然後按照補償政策繼續嘗試删除。
  • 每個緩存記錄設定過期事件,極端情況下,重試删除、補償删除等政策全部失敗時,等到資料記錄過期自動從緩存中淘汰,作為兜底政策。

這種處理方式,雖然依舊無法百分百保證資料一緻,但是整體出現資料不一緻情況的機率與可能性非常的小。

聊一聊安全且正确使用緩存的那些事——關于可靠性、關乎一緻性

實際使用場景中,對于一緻性要求不是特别高、且并發量不是特别大的場景,可以選擇基于資料庫事務保證的先更新資料庫再更新/删除緩存。而對于并發要求較高、且資料一緻性要求較好的時候,推薦選擇先更新資料庫,再删除緩存,并結合删除重試 + 補償邏輯 + 緩存過期TTL等綜合手段。

小結回顧

本篇内容中,我們主要探讨了下緩存的使用過程中的一些典型異常的觸發場景與防護政策,并一起聊了下保持緩存與資料庫資料一緻性的一些保障手段。

關于這些内容,我們本篇就聊到這裡。

那麼,你是否在使用緩存的時候遇到過類似的問題呢?你是如何解決這些問題的呢?你關于這些問題你是否有更好的了解與應對政策呢?歡迎評論區一起交流下,期待和各位小夥伴們一起切磋、共同成長。

補充說明 :

本文屬于《深入了解緩存原理與實戰設計》系列專欄的内容之一。該專欄圍繞緩存這個宏大命題進行展開闡述,全方位、系統性地深度剖析各種緩存實作政策與原理、以及緩存的各種用法、各種問題應對政策,并一起探讨下緩存設計的哲學。

如果有興趣,也歡迎關注此專欄。

聊一聊安全且正确使用緩存的那些事——關于可靠性、關乎一緻性

我是悟道,聊技術、又不僅僅聊技術~

如果覺得有用,請點贊 + 關注讓我感受到您的支援。也可以關注下我的公衆号【架構悟道】,擷取更及時的更新。

期待與你一起探讨,一起成長為更好的自己。

聊一聊安全且正确使用緩存的那些事——關于可靠性、關乎一緻性

繼續閱讀