通過前面的改造,你的電商系統(tǒng)在完成了對數(shù)據(jù)庫的主從分離和分庫分表之后,已經可以支撐十幾萬的DAU了,整體系統(tǒng)的架構也變成了下面這樣:

從整體看,數(shù)據(jù)庫分了主庫和從庫,數(shù)據(jù)也被切分到多個數(shù)據(jù)節(jié)點上。但隨著并發(fā)的增加,存儲數(shù)據(jù)量的增多,數(shù)據(jù)庫的磁盤IO逐漸成了系統(tǒng)的瓶頸,我們需要一種訪問更快的組件來降低請求響應時間,提升整體系統(tǒng)的性能。這時我們會使用緩存。什么是緩存?如何將它的優(yōu)勢最大化?
本節(jié)課是緩存篇的總綱,將從緩存定義、緩存分類和緩存優(yōu)勢劣勢三個方面帶你掌握緩存的設計思想和理念
什么是緩存
緩存,是一種存儲結構的組件,它的作用是讓對數(shù)據(jù)庫的請求更快的返回。
我們經常會把緩存放在內存中來存儲,所以有人就把內存和緩存畫上了等號,這時外行的。作為業(yè)內人士,要知道在某些場景下我們可能還會使用SSD作為冷數(shù)據(jù)的緩存。比如360開源的Pika就是使用SSD存儲數(shù)據(jù)解決redis的容量瓶頸的。
實際上,凡是位于速度相差較大的兩種硬件之間,用于協(xié)調兩者數(shù)據(jù)傳輸速度差異的結構,均可稱之為緩存。下面是常見硬件組件的延時情況。

從這些數(shù)據(jù)中,可以看到,做一次內存尋址大概需要100ns,而做一次磁盤的查找則需要10ms??梢娢覀兪褂脙却孀鳛榫彺娴拇鎯橘|相比于以磁盤為主要存儲介質的數(shù)據(jù)庫來說,性能上會提高多個數(shù)量級,同時也能夠支撐更高的并發(fā)量。所以內存是最常見的緩存數(shù)據(jù)的介質。
緩存作為一種常見的空間換時間的性能優(yōu)化手段,在很多地方都有應用,例子:
1.緩存案例
比如抖音,平臺上的短視頻實際上是使用內置的網絡播放器來完成的。網絡播放器接受的是數(shù)據(jù)流,將數(shù)據(jù)下載下來之后經過分離音視頻流,解碼等流程后輸出到外設設備上播放。
如果我們在打開一個視頻的時候才開始下載數(shù)據(jù)的話,無疑會增加視頻的打開時間(首播時間),并且播放的過程中會有卡頓。所以我們的播放器中通常會設計一些緩存的組件,在未打開視頻時緩存一部分視頻數(shù)據(jù),比如打開抖音,服務端可能一次會返回三個視頻信息,我們在播放第一個視頻的時候,播放器已經幫我們緩存了第二、3個視頻的部分數(shù)據(jù),這樣在看第二個視頻的時候可以給用戶"秒開"的感覺。
除此之外,我們熟知的HTTP協(xié)議也是有緩存機制的。當我們第一次請求靜態(tài)的資源時,比如一張圖片,服務端除了返回圖片信息,在響應頭里還有一個“Etag”的字段。瀏覽器會緩存圖片信息自己這個字段的值。當下一次再請求這個圖片的時候,瀏覽器發(fā)起的請求頭里面會有一個“If-None-Match”的字段,并且把緩存的Etag的值寫進去發(fā)給服務端。服務端比對圖片信息是否有變化,如果沒有,則返回瀏覽器一個304的狀態(tài)碼,瀏覽器會繼續(xù)使用緩存的圖片信息。通過這種緩存協(xié)商的方式,可以減少網路傳輸?shù)臄?shù)據(jù)大小,從而提升頁面展示的性能。

2.緩存與緩沖區(qū)
除了緩存,我們在日常開發(fā)過程中還會經常聽見一個相似的名詞——緩沖區(qū),那么,什么是緩沖區(qū)呢?
緩存可以提高低速設備的訪問速度,或者減少復雜耗時的計算帶來的性能問題。理論上來說,我們可以通過緩存解決所有關于“慢”的問題。比如從磁盤隨機讀取數(shù)據(jù)慢,從數(shù)據(jù)庫查詢數(shù)據(jù)慢,只是不同場景消耗的存儲成本不同。
緩沖區(qū)則是一塊臨時存儲數(shù)據(jù)的區(qū)域,這些數(shù)據(jù)后面會被傳輸?shù)狡渌O備上。緩沖區(qū)更像“消息隊列篇”中即將提到的消息隊列,用以彌補高速設備和低速設備通信時的速度差。比如我們將數(shù)據(jù)寫入磁盤時并不是直接刷盤,而是寫到一塊緩沖區(qū)里面,內核會標識這個緩沖區(qū)為臟。當經過一定時間或者臟緩沖區(qū)比例到一定閾值時,由單獨的線程把臟塊刷新到硬盤上。這樣避免了每次寫數(shù)據(jù)都要刷盤帶來的性能問題。

以上就是緩沖區(qū)和緩存的區(qū)別。
現(xiàn)在你已經了解了緩存的含義,那么我們經常使用的緩存有哪些呢?我們又如何使用緩存,將它的優(yōu)勢最大化呢?
緩存分類
常見的緩存主要是靜態(tài)緩存、分布式緩存和熱點本地緩存三種。
靜態(tài)花奴村在Web1.0時期是非常著名的,它一般通過生成Velocity模板或者靜態(tài)HTML文件來實現(xiàn)靜態(tài)緩存,在Nginx上部署靜態(tài)緩存可以減少對于后臺應用服務器的壓力。例如我在做一些內容管理系統(tǒng)的時候,后臺會錄入很多的文章,前臺在網站上展示文章內容,就像新浪網易這種門戶網站一樣。
當然我們也可以把文章錄入到數(shù)據(jù)庫里面,然后前端展示的時候穿透查詢數(shù)據(jù)庫來獲取數(shù)據(jù),但是這樣會對數(shù)據(jù)庫造成很大壓力。即使我們使用分布式緩存來擋讀請求,但是對于像日均PV幾十億的大型門戶網站來說,基于成本考慮仍然是不劃算的。
所以我們的解決思路是每篇文章在錄入的時候渲染成靜態(tài)頁面,放置在所有的前端Nginx等Web服務器上,這樣用戶在訪問的時候會優(yōu)先訪問Web服務器上的靜態(tài)頁面,再對舊的文章執(zhí)行一定的清理策略后,依然可以保證99%以上的緩存命中率。
這種緩存只能針對靜態(tài)數(shù)據(jù)來緩存,對于動態(tài)請求就無能為力了。那么我們如何針對動態(tài)請求做緩存呢?這時候就需要分布式緩存了。
我們平時熟悉的Memcached、redis就是分布式緩存的典型例子。它們性能強勁,通過一些分布式的方案組成集群可以突破單機的限制。所以在整體架構中,分布式緩存承擔著非常重要的角色。
靜態(tài)的資源緩存可以考慮靜態(tài)緩存,對于動態(tài)的請求可以選擇分布式緩存,什么時候需要考慮熱點本地緩存呢?
答案就是當我們遇到極端的熱點數(shù)據(jù)查詢的時候。熱點本地緩存主要部署在應用服務器的代碼中,用于阻擋熱點查詢對于分布式緩存節(jié)點或者數(shù)據(jù)庫的壓力。
比如某個明星結婚了這種無聊的新聞,吃瓜群眾回到ta的首頁圍觀,這就會引發(fā)這個用戶信息的熱點查詢。這些查詢通常會命中某一個緩存節(jié)點或者某一個數(shù)據(jù)庫分區(qū),短時間內會形成極高的熱點查詢。
那么我們會在代碼中使用一些本地緩存方案,如HashMap,Guava Cache或者Ehcache等,他們和應用程序部署在同一個進程中,優(yōu)勢是不需要跨網絡調度,速度極快,可以用來阻擋短時間內的熱點查詢。例子:
比如說你的電商系統(tǒng)的首頁有一些推薦的商品,這些商品信息是由編輯在后臺錄入和變更。你分析編輯錄入新的商品或者變更某個商品的信息后,在頁面的展示是允許有一些延遲的的,比如說30s的延遲,并且首頁請求量最大,即使使用分布式緩存也很難抗住,所以你決定使用Guava Cache來將所有的推薦商品的信息緩存起來,并且設置每隔30s重新從數(shù)據(jù)庫中加載最新的所有商品。
首先,我們初始化Guava的Loading Cache:
CacheBuilder<String, List<Product>> cacheBuilder = CacheBuilder.newBuilder().maximumSize(maxSize).recordStats(); //設置緩存最大值
cacheBuilder = cacheBuilder.refreshAfterWrite(30, TimeUnit.Seconds); //設置刷新間隔
LoadingCache<String, List<Product>> cache = cacheBuilder.build(new CacheLoader<String, List<Product>>() {
@Override
public List<Product> load(String k) throws Exception {
return productService.loadAll(); // 獲取所有商品
}
});
這樣你在獲取所有商品信息的時候可以調用Loading Cache的get方法,就可以優(yōu)先從本地緩存中獲取商品信息,如果本地緩存不存在,會使用CacheLoader中的洛基從數(shù)據(jù)庫中加載所有的商品。
由于本地緩存是部署在應用服務器中,而我們應用服務器通常會部署多臺,當數(shù)據(jù)更新時,我們不能確定哪臺服務器本地中了緩存,更新或者刪除所有服務器的緩存不是一個好的選擇,所以我們通常會等待緩存過期。因此,這種緩存的有效期很短,通常為分鐘或者秒級別,以避免返回前端臟數(shù)據(jù)。
緩存的不足
首先,緩存比較適合于讀多寫少的業(yè)務場景,并且數(shù)據(jù)最好帶有一定的熱點屬性。這是因為緩存畢竟會受限于存儲介質不可能緩存所有數(shù)據(jù)。
其次,緩存會給整體系統(tǒng)帶來復雜度,并且會有數(shù)據(jù)不一致的風險。當更新數(shù)據(jù)庫成功,更新緩存失敗的情況下,緩存在就會存在臟數(shù)據(jù),對于這種場景,我們可以考慮使用較短的過期時間或者手動清理的方式來解決。
緩存通常使用內存作為存儲介質,但是內存并不是無限的。
最后,緩存會給運維也帶來一定的成本。
雖然有這么多不足,但是緩存對于性能的提升是毋庸置疑的。做具體方案的時候需要對緩存的設計有更細致的思考才能最大化的發(fā)揮緩存的優(yōu)勢。