天天看點

緩存更新的幾個套路

前言

Hello,everybody,我是asong,上一篇文章我們一起聊一聊了面試中幾個常見的緩存問題,今天我依然聊一聊緩存,不過今天我們聊的不是面試了,我們一起來看一看我們在系統中緩存更新的設計,因自己經驗有限,是以這些緩存設計來源于網上,我隻是在這裡總結一下,有什麼不對的歡迎指出~~~😊。

緩存預熱 To solve 緩存冷啟動

在上一篇文章中[​​常見面試題之緩存雪崩、緩存穿透、緩存擊穿​​],忘記講了一個概念——緩存預熱,是以在這篇文章補充一下,開一個好頭,預熱嘛~~~。

什麼是緩存預熱呢?我們都知道平常在跑步前都要熱身,可以預防肌肉拉傷等一系例的好處。是以緩存預熱具有同樣的道理,我們的新系統上線後,我們可以将相關的緩存資料直接加載到緩存系統。這樣可以避免在使用者請求的時候,先去查詢資料庫,然後再将資料緩存的問題。使用者可以直接查詢事先已被預熱的緩存資料。其實緩存預熱是為了解決緩存冷啟動問題,我們新系統上線後,redis叢集啟動後,沒有任何的緩存資料,這就是redis的冷啟動。

緩存更新的幾個套路

如上圖所示,如果不進行預熱,那麼Redis初識狀态資料為空,系統上線初期,對于高并發的流量,都會通路到資料庫中,對資料庫造成流量的壓力。

如何解決

現在我們已經知道會有緩存預熱這個問題,那麼就要想一下對策咯。可以分析出以下兩點:

  • 需要統計通路頻度較高的熱點資料
  • 使用LRU資料删除政策,建構資料留存隊列

是以我們可以設計一個如下方案:

  • 首先,通過 nginx + lua 的方式,把通路流量資料上報到 Kafka,也可以是其它的 mq 隊列。
  • 然後使用實時計算架構(如 storm 、spark streaming、flume)從 kafka 中消費通路流量資料,實時計算出通路頻率高的資料,這裡統計出來的可能隻會有編号資訊,如商品編号或部落格編号等。
  • 最後,根據編号從 mysql 資料庫中查詢出具體的資訊,寫入 redis,開始提供服務。

緩存更新的幾種設計

1. 先删除緩存,在更新資料庫

雖然這是一種錯誤方法,但是這種設計也是屬于緩存更新的一種方法,是以大家還是要知道為什麼不可以這麼做。還是那句話:知其是以然嘛。

這種方法就是在更新資料庫時,先删除緩存,然後在更新資料庫,而後續的操作會把資料在裝載到緩存中,這種邏輯在并發時就會先髒資料,看如下圖:

緩存更新的幾個套路

我們解釋一下上圖的操作,兩個并發操作,一個是更新操作,另一個是查詢操作,更新操作删除緩存後,查詢操作沒有命中緩存,先把老資料讀出來後放到緩存中,然後更新操作更新了資料庫。于是,在緩存中的資料還是老的資料,導緻緩存中的資料是髒的,而且還一直這樣髒下去了。是以這個設計是錯誤的,不建議使用。

2. Cache aside

這是我們最常用的一種設計模式,其邏輯如下:

  • 查詢:程式先從cache中擷取資料,有資料直接傳回,沒有得到,則去資料庫中取資料,成功後更新到緩存中。
  • 更新:先把資料存到資料庫中,成功後,再讓緩存失效。
緩存更新的幾個套路

這種設計正好能解決上文出現髒資料的問題。我們來理一下,一個是查詢操作,一個是更新操作的并發,沒有了删除cache資料的操作了,而是先更新了資料庫中的資料,此時,緩存依舊有效,是以,并發的查詢操作拿的是沒有更新的資料,但是,更新操作馬上讓緩存的失效了,後續的查詢操作再把資料從資料庫中拉出來。而不會像文章開頭的那個邏輯産生的問題,後續的查詢操作一直都在取老的資料。

那麼是不是這種設計就不會存在并發問題了呢?不是的,比如,一個是讀操作,但是沒有命中緩存,然後就到資料庫中取資料,此時來了一個寫操作,寫完資料庫後,讓緩存失效,然後,之前的那個讀操作再把老的資料放進去,是以,會造成髒資料。但,這個case理論上會出現,不過,實際上出現的機率可能非常低,因為這個條件需要發生在讀緩存時緩存失效,而且并發着有一個寫操作。而實際上資料庫的寫操作會比讀操作慢得多,而且還要鎖表,而讀操作必需在寫操作前進入資料庫操作,而又要晚于寫操作更新緩存,所有的這些條件都具備的機率基本并不大。

我們可以為緩存設定上過期時間,這樣可以有效解決這個問題。

3. Read/Write Through

這個模式其實就是将 緩存服務 作為主要的存儲,應用的所有讀寫請求都是直接與緩存服務打交道,而不管最後端的資料庫了,資料庫的資料由緩存服務來維護和更新。不過緩存中資料變更的時候是同步去更新資料庫的,在應用的眼中隻有緩存服務。

流程如下:

  • Read Through

Read Through 套路就是在查詢操作中更新緩存,也就是說,當緩存失效的時候(過期或LRU換出),Cache Aside是由調用方負責把資料加載入緩存,而Read Through則用緩存服務自己來加載,進而對應用方是透明的。

  • Write Through

Write Through 套路和Read Through相仿,不過是在更新資料時發生。當有資料更新的時候,如果沒有命中緩存,直接更新資料庫,然後傳回。如果命中了緩存,則更新緩存,然後再由Cache自己更新資料庫(這是一個同步操作)

緩存更新的幾個套路

這個模式的特點就是出現髒資料的機率就比較低,但是就強依賴緩存了,對緩存服務的穩定性有較大要求,另外,增加新緩存節點時還會有初始狀态空資料問題。

4. Write Behind Caching

Write Behind Caching又叫做Write Back,就是在更新資料的時候,隻更新緩存,不更新資料庫,而緩存會異步地批量更新資料庫。這個設計的好處是讓資料的I/O操作可以很快,異步的操作還可以合并對同一個資料的多次操作,性能上是非常可觀的。

但是,其帶來的問題是,資料不是強一緻性的,而且可能會丢失。在軟體設計上,我們基本上不可能做出一個沒有缺陷的設計,就像算法設計中的時間換空間,空間換時間一個道理,有時候,強一緻性和高性能,高可用和高性性是有沖突的。軟體設計從來都是取舍Trade-Off。另外,Write Back實作邏輯比較複雜,因為他需要track有哪資料是被更新了的,需要刷到持久層上。作業系統的write back會在僅當這個cache需要失效的時候,才會被真正持久起來,比如,記憶體不夠了,或是程序退出了等情況,這又叫lazy write。

緩存更新的幾個套路

這個模式的特點就是速度很快,效率會非常高,但是資料的一緻性比較差,還可能會有資料的丢失情況,實作邏輯也較為複雜。

總結

上面講的這幾種緩存更新設計,都是一些前人使用的總結,這些設計也不是完美的,這個世界上沒有完美的設計,是以我們的設計多多少少會有問題,比如我們沒有考慮緩存(Cache)和持久層(Repository)的整體事務的問題。比如,更新Cache成功,更新資料庫失敗了怎麼嗎?或是反過來。關于這個事,如果你需要強一緻性,就要好好考慮怎麼解決這個問題。在軟體開發或設計中,我非常建議在之前先去參考一下已有的設計和思路,看看相應的guideline,best practice或design pattern,吃透了已有的這些東西,再決定是否要重新發明輪子。千萬不要似是而非地,想當然的做軟體設計。

好啦,這一篇文章到這裡就結束了,希望對你們有用,又不對的地方歡迎指出,可添加我的golang交流群,我們一起學習交流。

結尾給大家發一個小福利吧,最近我在看[微服務架構設計模式]這一本書,講的很好,自己也收集了一本PDF,有需要的小夥可以到自行下載下傳。擷取方式:關注公衆号:[Golang夢工廠],背景回複:[微服務],即可擷取。

我翻譯了一份GIN中文文檔,會定期進行維護,有需要的小夥伴背景回複[gin]即可下載下傳。