12 | 緩存:數(shù)據(jù)庫成為瓶頸后,動態(tài)數(shù)據(jù)的查詢要如何加速?

通過前面數(shù)據(jù)庫篇的學習,你已經(jīng)了解了在高并發(fā)大流量下,數(shù)據(jù)庫層的演進過程以及庫表設(shè)計上的考慮點。你的垂直電商系統(tǒng)在完成了對數(shù)據(jù)庫的主從分離和分庫分表之后,已經(jīng)可以支撐十幾萬 DAU 了,整體系統(tǒng)的架構(gòu)也變成了下面這樣:

img

從整體上看,數(shù)據(jù)庫分成了主庫和從庫,數(shù)據(jù)也被切分到多個數(shù)據(jù)庫節(jié)點上。但隨著并發(fā)的增加,存儲數(shù)據(jù)量的增多,數(shù)據(jù)庫的磁盤 IO 逐漸成了系統(tǒng)的瓶頸,我們需要一種訪問更快的組件來降低請求響應(yīng)時間,提升整體系統(tǒng)性能。這時我們就會使用緩存。那么什么是緩存,我們又該如何將它的優(yōu)勢最大化呢?

本節(jié)課是緩存篇的總綱,我將從緩存定義、緩存分類和緩存優(yōu)勢劣勢三個方面全方位帶你掌握緩存的設(shè)計思想和理念,再用剩下 4 節(jié)課的時間,帶你針對性地掌握使用緩存的正確姿勢,以便讓你在實際工作中能夠更好地使用緩存提升整體系統(tǒng)的性能。

什么是緩存

緩存,是一種存儲數(shù)據(jù)的組件,它的作用是讓對數(shù)據(jù)的請求更快地返回。

我們經(jīng)常會把緩存放在內(nèi)存中來存儲, 所以有人就把內(nèi)存和緩存畫上了等號,這完全是外行人的見解。作為業(yè)內(nèi)人士,你要知道在某些場景下我們可能還會使用 SSD 作為冷數(shù)據(jù)的緩存。比如說 360 開源的 Pika 就是使用 SSD 存儲數(shù)據(jù)解決 Redis 的容量瓶頸的。

實際上,凡是位于速度相差較大的兩種硬件之間,用于協(xié)調(diào)兩者數(shù)據(jù)傳輸速度差異的結(jié)構(gòu),均可稱之為緩存。那么說到這兒我們就需要知道常見硬件組件的延時情況是什么樣的了,這樣在做方案的時候可以對延遲有更直觀的印象。幸運的是,業(yè)內(nèi)已經(jīng)有人幫我們總結(jié)出這些數(shù)據(jù)了,我將這些數(shù)據(jù)整理了一下,你可以看一下。

img

從這些數(shù)據(jù)中,你可以看到,做一次內(nèi)存尋址大概需要 100ns,而做一次磁盤的查找則需要 10ms。如果我們將做一次內(nèi)存尋址的時間類比為一個課間,那么做一次磁盤查找相當于度過了大學的一個學期。可見,我們使用內(nèi)存作為緩存的存儲介質(zhì)相比于以磁盤作為主要存儲介質(zhì)的數(shù)據(jù)庫來說,性能上會提高多個數(shù)量級,同時也能夠支撐更高的并發(fā)量。所以,內(nèi)存是最常見的一種緩存數(shù)據(jù)的介質(zhì)。

緩存作為一種常見的空間換時間的性能優(yōu)化手段,在很多地方都有應(yīng)用,我們先來看幾個例子,相信你一定不會陌生。

1、緩存案例

Linux 內(nèi)存管理是通過一個叫做 MMU(Memory Management Unit)的硬件,來實現(xiàn)從虛擬地址到物理地址的轉(zhuǎn)換的,但是如果每次轉(zhuǎn)換都要做這么復(fù)雜計算的話,無疑會造成性能的損耗,所以我們會借助一個叫做 TLB(Translation Lookaside Buffer)的組件來緩存最近轉(zhuǎn)換過的虛擬地址,和物理地址的映射。TLB 就是一種緩存組件,緩存復(fù)雜運算的結(jié)果,就好比你做一碗色香味俱全的面條可能比較復(fù)雜,那么我們把做好的面條油炸處理一下做成方便面,你做方便面的話就簡單多了,也快速多了。這個緩存組件比較底層,這里你只需要了解一下就可以了。

在大部分的筆記本,桌面電腦和服務(wù)器上都會有一個或者多個 TLB 組件,在不經(jīng)意間幫助我們加快地址轉(zhuǎn)換的速度。

再想一下你平時經(jīng)常刷的抖音。平臺上的短視頻實際上是使用內(nèi)置的網(wǎng)絡(luò)播放器來完成的。網(wǎng)絡(luò)播放器接收的是數(shù)據(jù)流,將數(shù)據(jù)下載下來之后經(jīng)過分離音視頻流,解碼等流程后輸出到外設(shè)設(shè)備上播放。

如果我們在打開一個視頻的時候才開始下載數(shù)據(jù)的話,無疑會增加視頻的打開速度(我們叫首播時間),并且播放過程中會有卡頓。所以我們的播放器中通常會設(shè)計一些緩存的組件,在未打開視頻時緩存一部分視頻數(shù)據(jù),比如我們打開抖音,服務(wù)端可能一次會返回三個視頻信息,我們在播放第一個視頻的時候,播放器已經(jīng)幫我們緩存了第二、三個視頻的部分數(shù)據(jù),這樣在看第二個視頻的時候就可以給用戶“秒開”的感覺。

除此之外,我們熟知的 HTTP 協(xié)議也是有緩存機制的。當我們第一次請求靜態(tài)的資源時,比如一張圖片,服務(wù)端除了返回圖片信息,在響應(yīng)頭里面還有一個“Etag”的字段。瀏覽器會緩存圖片信息以及這個字段的值。當下一次再請求這個圖片的時候,瀏覽器發(fā)起的請求頭里面會有一個“If-None-Match”的字段,并且把緩存的“Etag”的值寫進去發(fā)給服務(wù)端。服務(wù)端比對圖片信息是否有變化,如果沒有,則返回瀏覽器一個 304 的狀態(tài)碼,瀏覽器會繼續(xù)使用緩存的圖片信息。通過這種緩存協(xié)商的方式,可以減少網(wǎng)絡(luò)傳輸?shù)臄?shù)據(jù)大小,從而提升頁面展示的性能。

img

2、緩存與緩沖區(qū)

講了這么多緩存案例,想必你對緩存已經(jīng)有了一個直觀并且形象的了解了。除了緩存,我們在日常開發(fā)過程中還會經(jīng)常聽見一個相似的名詞——緩沖區(qū),那么,什么是緩沖區(qū)呢?緩沖和緩存只有一字之差,它們有什么區(qū)別呢?

我們知道,緩存可以提高低速設(shè)備的訪問速度,或者減少復(fù)雜耗時的計算帶來的性能問題。理論上說,我們可以通過緩存解決所有關(guān)于“慢”的問題,比如從磁盤隨機讀取數(shù)據(jù)慢,從數(shù)據(jù)庫查詢數(shù)據(jù)慢,只是不同的場景消耗的存儲成本不同。

緩沖區(qū)則是一塊臨時存儲數(shù)據(jù)的區(qū)域,這些數(shù)據(jù)后面會被傳輸?shù)狡渌O(shè)備上。緩沖區(qū)更像“消息隊列篇”中即將提到的消息隊列,用以彌補高速設(shè)備和低速設(shè)備通信時的速度差。比如,我們將數(shù)據(jù)寫入磁盤時并不是直接刷盤,而是寫到一塊緩沖區(qū)里面,內(nèi)核會標識這個緩沖區(qū)為臟。當經(jīng)過一定時間或者臟緩沖區(qū)比例到達一定閾值時,由單獨的線程把臟塊刷新到硬盤上。這樣避免了每次寫數(shù)據(jù)都要刷盤帶來的性能問題。

img

以上就是緩沖區(qū)和緩存的區(qū)別,從這個區(qū)別來看,上面提到的 TLB 的命名是有問題的,它應(yīng)該是緩存而不是緩沖區(qū)。

現(xiàn)在你已經(jīng)了解了緩存的含義,那么我們經(jīng)常使用的緩存都有哪些?我們又該如何使用緩存,將它的優(yōu)勢最大化呢?

緩存分類

在我們?nèi)粘i_發(fā)中,常見的緩存主要就是靜態(tài)緩存、分布式緩存和熱點本地緩存這三種。

靜態(tài)緩存在 Web 1.0 時期是非常著名的,它一般通過生成 Velocity 模板或者靜態(tài) HTML 文件來實現(xiàn)靜態(tài)緩存,在 Nginx 上部署靜態(tài)緩存可以減少對于后臺應(yīng)用服務(wù)器的壓力。例如,我們在做一些內(nèi)容管理系統(tǒng)的時候,后臺會錄入很多的文章,前臺在網(wǎng)站上展示文章內(nèi)容,就像新浪,網(wǎng)易這種門戶網(wǎng)站一樣。

當然,我們也可以把文章錄入到數(shù)據(jù)庫里面,然后前端展示的時候穿透查詢數(shù)據(jù)庫來獲取數(shù)據(jù),但是這樣會對數(shù)據(jù)庫造成很大的壓力。雖然我們使用分布式緩存來擋讀請求,但是對于像日均 PV 幾十億的大型門戶網(wǎng)站來說,基于成本考慮仍然是不劃算的。

所以我們的解決思路是每篇文章在錄入的時候渲染成靜態(tài)頁面,放置在所有的前端 Nginx 或者 Squid 等 Web 服務(wù)器上,這樣用戶在訪問的時候會優(yōu)先訪問 Web 服務(wù)器上的靜態(tài)頁面,在對舊的文章執(zhí)行一定的清理策略后,依然可以保證 99% 以上的緩存命中率。

這種緩存只能針對靜態(tài)數(shù)據(jù)來緩存,對于動態(tài)請求就無能為力了。那么我們?nèi)绾吾槍討B(tài)請求做緩存呢?這時你就需要分布式緩存了。

分布式緩存的大名可謂是如雷貫耳了,我們平時耳熟能詳?shù)?Memcached、Redis 就是分布式緩存的典型例子。它們性能強勁,通過一些分布式的方案組成集群可以突破單機的限制。所以在整體架構(gòu)中,分布式緩存承擔著非常重要的角色(接下來的課程我會專門針對分布式緩存,帶你了解分布式緩存的使用技巧以及高可用的方案,讓你能在工作中對分布式緩存運用自如)。

對于靜態(tài)的資源的緩存你可以選擇靜態(tài)緩存,對于動態(tài)的請求你可以選擇分布式緩存,那么什么時候要考慮熱點本地緩存呢?

答案是當我們遇到極端的熱點數(shù)據(jù)查詢的時候。熱點本地緩存主要部署在應(yīng)用服務(wù)器的代碼中,用于阻擋熱點查詢對于分布式緩存節(jié)點或者數(shù)據(jù)庫的壓力。

比如某一位明星在微博上有了熱點話題,“吃瓜群眾”會到他 (她) 的微博首頁圍觀,這就會引發(fā)這個用戶信息的熱點查詢。這些查詢通常會命中某一個緩存節(jié)點或者某一個數(shù)據(jù)庫分區(qū),短時間內(nèi)會形成極高的熱點查詢。

那么我們會在代碼中使用一些本地緩存方案,如 HashMap,Guava Cache 或者是 Ehcache 等,它們和應(yīng)用程序部署在同一個進程中,優(yōu)勢是不需要跨網(wǎng)絡(luò)調(diào)度,速度極快,所以可以用來阻擋短時間內(nèi)的熱點查詢。來看個例子。

比方說你的垂直電商系統(tǒng)的首頁有一些推薦的商品,這些商品信息是由編輯在后臺錄入和變更。你分析編輯錄入新的商品或者變更某個商品的信息后,在頁面的展示是允許有一些延遲的,比如說 30 秒的延遲,并且首頁請求量最大,即使使用分布式緩存也很難抗住,所以你決定使用 Guava Cache 來將所有的推薦商品的信息緩存起來,并且設(shè)置每隔 30 秒重新從數(shù)據(jù)庫中加載最新的所有商品。

首先,我們初始化 Guava 的 Loading Cache:

CacheBuilder> cacheBuilder = CacheBuilder.newBuilder().maximumSize(maxSize).recordStats(); //設(shè)置緩存最大值
cacheBuilder = cacheBuilder.refreshAfterWrite(30, TimeUnit.Seconds); 
//設(shè)置刷新間隔
LoadingCache> cache = cacheBuilder.build(new CacheLoader>() { @Override public List load(String k) throws Exception { return productService.loadAll(); // 獲取所有商品 }});

這樣,你在獲取所有商品信息的時候可以調(diào)用 Loading Cache 的 get 方法,就可以優(yōu)先從本地緩存中獲取商品信息,如果本地緩存不存在,會使用 CacheLoader 中的邏輯從數(shù)據(jù)庫中加載所有的商品。

由于本地緩存是部署在應(yīng)用服務(wù)器中,而我們應(yīng)用服務(wù)器通常會部署多臺,當數(shù)據(jù)更新時,我們不能確定哪臺服務(wù)器本地中了緩存,更新或者刪除所有服務(wù)器的緩存不是一個好的選擇,所以我們通常會等待緩存過期。因此,這種緩存的有效期很短,通常為分鐘或者秒級別,以避免返回前端臟數(shù)據(jù)。

緩存的不足

通過了解上面的內(nèi)容,你不難發(fā)現(xiàn),緩存的主要作用是提升訪問速度,從而能夠抗住更高的并發(fā)。那么,緩存是不是能夠解決一切問題?顯然不是。事物都是具有兩面性的,緩存也不例外,我們要了解它的優(yōu)勢的同時也需要了解它有哪些不足,從而揚長避短,將它的作用發(fā)揮到最大。

首先,緩存比較適合于讀多寫少的業(yè)務(wù)場景,并且數(shù)據(jù)最好帶有一定的熱點屬性。這是因為緩存畢竟會受限于存儲介質(zhì)不可能緩存所有數(shù)據(jù),那么當數(shù)據(jù)有熱點屬性的時候才能保證一定的緩存命中率。比如說類似微博、朋友圈這種 20% 的內(nèi)容會占到 80% 的流量。所以,一旦當業(yè)務(wù)場景讀少寫多時或者沒有明顯熱點時,比如在搜索的場景下,每個人搜索的詞都會不同,沒有明顯的熱點,那么這時緩存的作用就不明顯了。

其次,緩存會給整體系統(tǒng)帶來復(fù)雜度,并且會有數(shù)據(jù)不一致的風險。當更新數(shù)據(jù)庫成功,更新緩存失敗的場景下,緩存中就會存在臟數(shù)據(jù)。對于這種場景,我們可以考慮使用較短的過期時間或者手動清理的方式來解決。

再次,之前提到緩存通常使用內(nèi)存作為存儲介質(zhì),但是內(nèi)存并不是無限的。因此,我們在使用緩存的時候要做數(shù)據(jù)存儲量級的評估,對于可預(yù)見的需要消耗極大存儲成本的數(shù)據(jù),要慎用緩存方案。同時,緩存一定要設(shè)置過期時間,這樣可以保證緩存中的會是熱點數(shù)據(jù)。

最后,緩存會給運維也帶來一定的成本,運維需要對緩存組件有一定的了解,在排查問題的時候也多了一個組件需要考慮在內(nèi)。

雖然有這么多的不足,但是緩存對于性能的提升是毋庸置疑的,我們在做架構(gòu)設(shè)計的時候也需要把它考慮在內(nèi),只是在做具體方案的時候需要對緩存的設(shè)計有更細致的思考,才能最大化地發(fā)揮緩存的優(yōu)勢。

這節(jié)課我?guī)懔私饬司彺娴亩x,常見緩存的分類以及緩存的不足。我想跟你強調(diào)的重點有以下幾點:

  • 緩存可以有多層,比如上面提到的靜態(tài)緩存處在負載均衡層,分布式緩存處在應(yīng)用層和數(shù)據(jù)庫層之間,本地緩存處在應(yīng)用層。我們需要將請求盡量擋在上層,因為越往下層,對于并發(fā)的承受能力越差;
  • 緩存命中率是我們對于緩存最重要的一個監(jiān)控項,越是熱點的數(shù)據(jù),緩存的命中率就越高。

你還需要理解的是,緩存不僅僅是一種組件的名字,更是一種設(shè)計思想,你可以認為任何能夠加速讀請求的組件和設(shè)計方案都是緩存思想的體現(xiàn)。而這種加速通常是通過兩種方式來實現(xiàn):

  • 使用更快的介質(zhì),比方說課程中提到的內(nèi)存;
  • 緩存復(fù)雜運算的結(jié)果,比方說前面 TLB 的例子就是緩存地址轉(zhuǎn)換的結(jié)果。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容