1. 程式人生 > 資料庫 >關於快取的一些重要概念(Redis 前置菜)

關於快取的一些重要概念(Redis 前置菜)

1. 快取的基本思想

很多朋友,只知道快取可以提高系統性能以及減少請求相應時間,但是,不太清楚快取的本質思想是什麼。

快取的基本思想其實很簡單,就是我們非常熟悉的空間換時間。不要把快取想的太高大上,雖然,它的確對系統的效能提升的價效比非常高。

其實,我們在學習使用快取的時候,你會發現快取的思想實際在作業系統或者其他地方都被大量用到。 比如 CPU Cache 快取的是記憶體資料用於解決 CPU 處理速度和記憶體不匹配的問題,記憶體快取的是硬碟資料用於解決硬碟訪問速度過慢的問題。 再比如作業系統在 頁表方案 基礎之上引入了 快表 來加速虛擬地址到實體地址的轉換。我們可以把快表理解為一種特殊的高速緩衝儲存器(Cache)。

迴歸到業務系統來說:我們為了避免使用者在請求資料的時候獲取速度過於緩慢,所以我們在資料庫之上增加了快取這一層來彌補。

當別人再問你,快取的基本思想的時候,就把上面這段話告訴他,我覺得會讓別人對你刮目相看。

2. 使用快取為系統帶來了什麼問題

軟體系統設計中沒有銀彈,往往任何技術的引入都像是把雙刃劍。 你使用的方式得當,就能為系統帶來很大的收益。否則,只是費了精力不討好。

簡單來說,為系統引入快取之後往往會帶來下面這些問題:

ps:其實我覺得引入本地快取來做一些簡單業務場景的話,實際帶來的代價幾乎可以忽略,下面主要是針對分散式快取來說的。

  1. 系統複雜性增加 :引入快取之後,你要維護快取和資料庫的資料一致性、維護熱點快取等等。
  2. 系統開發成本往往會增加 :引入快取意味著系統需要一個單獨的快取服務,這是需要花費相應的成本的,並且這個成本還是很貴的,畢竟耗費的是寶貴的記憶體。但是,如果你只是簡單的使用一下本地快取儲存一下簡單的資料,並且資料量不大的話,那麼就不需要單獨去弄一個快取服務。

3. 本地快取解決方案

先來聊聊本地快取,這個實際在很多專案中用的蠻多,特別是單體架構的時候。資料量不大,並且沒有分散式要求的話,使用本地快取還是可以的。

常見的單體架構圖如下,我們使用 Nginx 來做負載均衡,部署兩個相同的服務到伺服器,兩個服務使用同一個資料庫,並且使用的是本地快取。

那本地快取的方案有哪些呢?且聽 Guide 給你來說一說。

一:JDK 自帶的 HashMapConcurrentHashMap 了。

ConcurrentHashMap 可以看作是執行緒安全版本的 HashMap ,兩者都是存放 key/value 形式的鍵值對。但是,大部分場景來說不會使用這兩者當做快取,因為只提供了快取的功能,並沒有提供其他諸如過期時間之類的功能。一個稍微完善一點的快取框架至少要提供:過期時間淘汰機制命中率統計這三點。

二: EhcacheGuava CacheSpring Cache 這三者是使用的比較多的本地快取框架。

  • Ehcache 的話相比於其他兩者更加重量。不過,相比於 Guava CacheSpring Cache 來說, Ehcache 支援可以嵌入到 hibernate 和 mybatis 作為多級快取,並且可以將快取的資料持久化到本地磁碟中、同時也提供了叢集方案(比較雞肋,可忽略)。
  • Guava CacheSpring Cache 兩者的話比較像。Guava 相比於 Spring Cache 的話使用的更多一點,它提供了 API 非常方便我們使用,同時也提供了設定快取有效時間等功能。它的內部實現也比較乾淨,很多地方都和 ConcurrentHashMap 的思想有異曲同工之妙。
  • 使用 Spring Cache 的註解實現快取的話,程式碼會看著很乾淨和優雅,但是很容易出現問題比如快取穿透、記憶體溢位。

三: 後起之秀 Caffeine。

相比於 Guava 來說 Caffeine 在各個方面比如效能要更加優秀,一般建議使用其來替代 Guava 。並且, GuavaCaffeine 的使用方式很像!

本地快取固然好,但是缺陷也很明顯,比如多個相同服務之間的本地快取的資料無法共享。

4. 為什麼要有分散式快取?/為什麼不直接用本地快取?

本地的快取的優勢非常明顯:低依賴輕量簡單成本低

但是,本地快取存在下面這些缺陷:

  1. 本地快取對分散式架構支援不友好,比如同一個相同的服務部署在多臺機器上的時候,各個服務之間的快取是無法共享的,因為本地快取只在當前機器上有。
  2. 本地快取容量受服務部署所在的機器限制明顯。 如果當前系統服務所耗費的記憶體多,那麼本地快取可用的容量就很少。

我們可以把分散式快取(Distributed Cache) 看作是一種記憶體資料庫的服務,它的最終作用就是提供快取資料的服務。

如下圖所示,就是一個簡單的使用分散式快取的架構圖。我們使用 Nginx 來做負載均衡,部署兩個相同的服務到伺服器,兩個服務使用同一個資料庫和快取。

使用分散式快取之後,快取部署在一臺單獨的伺服器上,即使同一個相同的服務部署在再多機器上,也是使用的同一份快取。 並且,單獨的分散式快取服務的效能、容量和提供的功能都要更加強大。

使用分散式快取的缺點呢,也很顯而易見,那就是你需要為分散式快取引入額外的服務比如 Redis 或 Memcached,你需要單獨保證 Redis 或 Memcached 服務的高可用。

5. 快取讀寫模式/更新策略

下面介紹到的三種模式各有優劣,不存在最佳模式,根據具體的業務場景選擇適合自己的快取讀寫模式。

5.1. Cache Aside Pattern(旁路快取模式)

Cache Aside Pattern 是我們平時使用比較多的一個快取讀寫模式,比較適合讀請求比較多的場景。

Cache Aside Pattern 中服務端需要同時維繫 DB 和 cache,並且是以 DB 的結果為準。

下面我們來看一下這個策略模式下的快取讀寫步驟。

  • 先更新 DB
  • 然後直接刪除 cache 。

簡單畫了一張圖幫助大家理解寫的步驟。

:

  • 從 cache 中讀取資料,讀取到就直接返回
  • cache中讀取不到的話,就從 DB 中讀取資料返回
  • 再把資料放到 cache 中。

簡單畫了一張圖幫助大家理解讀的步驟。

你僅僅瞭解了上面這些內容的話是遠遠不夠的,我們還要搞懂其中的原理。

比如說面試官很可能會追問:“在寫資料的過程中,可以先刪除 cache ,後更新 DB 麼?

答案: 那肯定是不行的!因為這樣可能會造成資料庫(DB)和快取(Cache)資料不一致的問題。為什麼呢?比如說請求1 先寫資料A,請求2隨後讀資料A的話就很有可能產生資料不一致性的問題。這個過程可以簡單描述為:

請求1先把cache中的A資料刪除 -> 請求2從DB中讀取資料->請求1再把DB中的A資料更新。

當你這樣回答之後,面試官可能會緊接著就追問:“在寫資料的過程中,先更新DB,後刪除cache就沒有問題了麼?

答案: 理論上來說還是可能會出現資料不一致性的問題,不過概率非常小,因為快取的寫入速度是比資料庫的寫入速度快很多!

比如請求1先讀資料 A,請求2隨後寫資料A,並且資料A不在快取中的話也有可能產生資料不一致性的問題。這個過程可以簡單描述為:

請求1從DB讀資料A->請求2寫更新資料 A 到資料庫並把刪除cache中的A資料->請求1將資料A寫入cache。

現在我們再來分析一下 Cache Aside Pattern 的缺陷

缺陷1:首次請求資料一定不在 cache 的問題

解決辦法:可以將熱點資料可以提前放入cache 中。

缺陷2:寫操作比較頻繁的話導致cache中的資料會被頻繁被刪除,這樣會影響快取命中率 。

解決辦法:

  • 資料庫和快取資料強一致場景 :更新DB的時候同樣更新cache,不過我們需要加一個鎖/分散式鎖來保證更新cache的時候不存線上程安全問題。
  • 可以短暫地允許資料庫和快取資料不一致的場景 :更新DB的時候同樣更新cache,但是給快取加一個比較短的過期時間,這樣的話就可以保證即使資料不一致的話影響也比較小。

5.2. Read/Write Through Pattern(讀寫穿透)

Read/Write Through Pattern 中服務端把 cache 視為主要資料儲存,從中讀取資料並將資料寫入其中。cache 服務負責將此資料讀取和寫入 DB,從而減輕了應用程式的職責。

這種快取讀寫策略小夥伴們應該也發現了在平時在開發過程中非常少見。拋去效能方面的影響,大概率是因為我們經常使用的分散式快取 Redis 並沒有提供 cache 將資料寫入DB的功能。

寫(Write Through):

  • 先查 cache,cache 中不存在,直接更新 DB。
  • cache 中存在,則先更新 cache,然後 cache 服務自己更新 DB(同步更新 cache 和 DB)。

簡單畫了一張圖幫助大家理解寫的步驟。

讀(Read Through):

  • 從 cache 中讀取資料,讀取到就直接返回 。
  • 讀取不到的話,先從 DB 載入,寫入到 cache 後返回響應。

簡單畫了一張圖幫助大家理解讀的步驟。

Read-Through Pattern 實際只是在 Cache-Aside Pattern 之上進行了封裝。在 Cache-Aside Pattern 下,發生讀請求的時候,如果 cache 中不存在對應的資料,是由客戶端自己負責把資料寫入 cache,而 Read Through Pattern 則是 cache 服務自己來寫入快取的,這對客戶端是透明的。

和 Cache Aside Pattern 一樣, Read-Through Pattern 也有首次請求資料一定不再 cache 的問題,對於熱點資料可以提前放入快取中。

5.3. Write Behind Pattern(非同步快取寫入)

Write Behind Pattern 和 Read/Write Through Pattern 很相似,兩者都是由 cache 服務來負責 cache 和 DB 的讀寫。

但是,兩個又有很大的不同:Read/Write Through 是同步更新 cache 和 DB,而 Write Behind Caching 則是隻更新快取,不直接更新 DB,而是改為非同步批量的方式來更新 DB。

很明顯,這種方式對資料一致性帶來了更大的挑戰,比如cache資料可能還沒非同步更新DB的話,cache服務可能就就掛掉了。

這種策略在我們平時開發過程中也非常非常少見,但是不代表它的應用場景少,比如訊息佇列中訊息的非同步寫入磁碟、MySQL 的 InnoDB Buffer Pool 機制都用到了這種策略。

Write Behind Pattern 下 DB 的寫效能非常高,非常適合一些資料經常變化又對資料一致性要求沒那麼高的場景,比如瀏覽量、點贊量。