緩存提升性能的關鍵性手段


  • 提高「性能」的主要方式是優化,而優化的其中一個主要手段就是添加緩存!

    在軟件工程里有這么一句話:「沒有銀彈」!就是說由於軟件工程的復雜性,沒有任何一種技術或方法能解決所有問題!軟件工程是復雜的,沒有銀彈!但是,軟件工程中的某一個問題,是有銀彈的!

    「 計算機科學領域的任何問題都可以通過增加一個間接的中間層來解決」!而「緩存層」可以說是添加得最多的層!主要目的就是為了提高性能!所以,緩存可以說是「性能銀彈」!

    本文將探討如下內容:

    • 緩存的作用
    • 緩存的種類
    • 緩存算法
    • 分布式緩存
    • 緩存的使用
    • 網絡中的緩存
    • 應用緩存
    • 數據庫緩存
    • 計算機中的緩存

    從代碼說起

    fn longRunningOperations(){ ... // 很耗時}let result = longRunningOperations();// do other thing
    

      我們來看上面這段偽代碼,longRunningOperations是個很耗時的方法(調用一次要幾十秒甚至幾分鍾),比如:

    • 復雜的業務邏輯計算
    • 復雜的數據查詢
    • 耗時的網絡操作等

    對於這個方法,如果每次都去調用一次的話,會非常的影響性能,用戶體驗也非常的不好。

    那我們該如何處理呢?

    一般有幾種優化方案:

    • 優化業務代碼,比如:更快的數據結構和算法,更快的IO模型,建立數據庫索引等
    • 簡化業務邏輯,導致耗時的原因可能是業務過於復雜,可以通過簡化業務邏輯的方式來減少耗時
    • 將操作的結果存儲起來。例如:對於某些統計類的結果,可以先用日終定時的去執行,將結果存儲到統計結果表中,查找時,直接從結果中查詢即可;對於某些臨時操作,可以將結果存儲在內存中,再次調用時,直接從內存中獲取即可。

    本文主要聊聊第三種方案:使用「緩存」!

    主動緩存與被動緩存

    一般我們使用緩存來存儲一些內容,這些內容有如下一些特點(符合一條或多條):

    • 使用較頻繁
    • 變更不頻繁
    • 獲取較耗時
    • 多系統訪問

    比如,

    • 字典數據:系統很多地方都會使用字典數據,而字典數據配置完成后一般不會修改,雖然從數據庫中直接獲取字典數據不是很耗時,但是多了查詢和網絡傳輸,性能上還是不如直接從緩存里面取快速
    • 秒殺商品信息:在秒殺時訪問量很大,從緩存(靜態文件、CDN等)獲取要比從數據庫查詢要快得多
    • 其它訪問頻次較多的信息:此處的其它信息是因為其緩存的處理方式與上面的字典處理有差異,下面詳細說明。

    對於字典數據來說,一般我們的做法是在系統啟動時,將字典數據直接加載到緩存中,此類緩存數據一般沒有過期時間;當修改字典時,會同時更新緩存中的內容。此類緩存稱為「主動緩存」,因為其緩存數據是由用戶的主動修改來觸發更新的。

    而對於某些信息來說,因為信息量太大,不能一次性全部加載到緩存中,且也不是太清楚哪些數據訪問頻次高、哪些數據訪問頻次低。對於這樣的數據,一般的做法是:

    • 先到緩存中查找是否有訪問的數據,如果有則直接返回給用戶
    • 如果沒有,則去溯源查找
    • 找到后將其添加到緩存中
    • 最后返回給用戶

    此類緩存稱為「被動緩存」!其緩存的數據的過期由系統來控制。那系統如何控制呢?這就涉及到緩存置換算法!

    緩存置換算法

    上面說了,對於被動緩存來說,由於信息量太大,數據不能一次全部加載到緩存中,當緩存滿了以后,需要新增數據時,就需要確定哪些數據要從緩存里清除,給新數據騰出空間。

    用於判斷哪些數據優先從緩存中剔除的算法稱為「緩存(頁面)置換算法」!

    Wiki中列出了如下置換算法:

    • RR(Random replacement)
    • FIFO(First in first out)
    • LIFO(Last in first out)
    • MRU(Most recently used)
    • LFU(Least-frequently used)
    • LRU(Least recently used)
    • TLRU(Time aware least recently used)
    • PLRU(Pseudo-LRU)
    • LRU-K
    • SLRU(Segmented LRU)
    • MQ(Multi queue)
    • LFRU(Least frequent recently used)
    • LFUDA(LFU with dynamic aging)
    • LIRS(Low inter-reference recency set)
    • ARC(Adaptive replacement cache)
    • CAR(Clock with adaptive replacement)
    • Pannier(Container-based caching algorithm for compound objects)

    一般情況下我們不會自己去實現個緩存,市面上有不少開源的緩存中間件,比如:redis,memcached。這里只簡單的梳理幾個常用的置換算法。

    FIFO

    FIFO應該算是最簡單的置換算法了:

    • 它使用一個隊列來維護數據
    • 數據按照加載到緩存的順序進行排列,先加載的數據在隊列頭部,后加載的數據在隊列尾部
    • 當緩存滿了以后,從隊列頭部清除數據,給需要加載的數據騰出空間
    • 新數據加到隊列的尾部

    FIFO的實現很簡單,但是其性能並不總是很好。舉個簡單的例子,假設一個系統需要10個緩存數據,恰巧此時5個數據在隊列頭部,另外5個數據不在緩存中,又恰巧此時隊列又滿了。按照FIFO算法,5條不在內存中的數據被加載到了緩存中,而之前的5條數據被清除了。這就需要再次將被清除的5條數據加載到緩存中。這就影響了性能。

    這個問題可能會隨着所分配的緩存大小的增加而增加,原本我們使用緩存是為了提高性能的,現在可能會影響性能,這種現象稱為「Belady現象」!

    LIFO和FIFO很類似,這里就不贅述了。

    LRU

    目前比較常用的置換算法稱為LRU置換算法:優先替換掉「最近最少使用」的數據

    • 每個數據都被關聯了該數據上次使用的時間
    • 當需要置換數據的時候,LRU選擇最長時間沒有使用的數據

    LRU的變體有很多,例如:

    • TLRU(Time aware least recently used):大部分緩存數據是有過期時間的。PLRU從最少使用和過期時間兩個維度來置換數據
    • LRU-K:多維護一個隊列,用於記錄所有緩存數據被訪問的次數。當數據的訪問次數達到K次的時候,才將數據放入緩存。當需要淘汰數據時,LRU-K會淘汰第K次訪問時間距當前時間最大的數據。
    • SLRU(Segmented LRU)2Queue?:一個FIFO隊列,一個LRU隊列。當數據第一次訪問時,將數據緩存在FIFO隊列里面,當數據第二次被訪問時,則將數據從FIFO隊列移到LRU隊列里面,兩個隊列各自按照自己的方法淘汰數據。
    • PLRU(Pseudo-LRU):LRU需要維護數據訪問時間,占用了額外的空間,對於空間很小的設備來說,此算法太過浪費空間了。PLRU每個緩存數據只需要1bit來存儲數據信息,可以達到LRU的效果。具體流程見下圖:

    還有和LRU類似的MRU,LFU這里不在贅述!

    緩存集群

    為了提高緩存的可用性,一般我們至少會對緩存做個主備,即一個主緩存,一個從緩存。

    • 緩存的寫入只可以寫到主緩存
    • 主緩存同步數據到從緩存中
    • 可以從主緩存讀取數據。也可以從從緩存讀取數據(不必須)
    • 當主緩存掛掉了,從緩存升級為主緩存

    再安全一點的做法就是做緩存集群:

    • 多台機器緩存了相同的數據,其中一台為主緩存
    • 緩存的寫入只可以寫到主緩存
    • 主緩存同步數據到其它緩存
    • 可以從主緩存讀取數據。也可以從其它緩存中讀取數據(不必須)
    • 當主緩存掛掉了,會從其它緩存服務中選擇一個作為新的主緩存

    分布式緩存

    無論是單機緩存,主從備份還是緩存集群,都沒法解決緩存大小限制的問題。因為一般緩存會使用內存,而一台機器的內存大小是有限的。當需要緩存的數據遠遠超過一台機器的內存大小的時候,就需要將緩存的數據分布到多台機器上。每台機器只緩存一部分數據,這就是分布式緩存。

    分布式緩存可以解決一台機器緩存數據有限的問題,但是也引入了新的問題:

    • 哪些數據該緩存在哪台服務器上
    • 如何保證每台服務器緩存的數據量基本相同

    一般做法是對key進行hash,然后對服務器數量進行取余,來確定數據在哪台服務器上。這解決了「哪些數據該緩存在哪台服務器上」的問題,但是卻無法保證「每台服務器緩存的數據量基本相同」,因為可能多個key的hash取余后都落到了同一個服務器上,這就可能導致其中一台服務器緩存的數量很多,其它服務器緩存的數據量很少。緩存數據量多的服務器可能會內存不夠用,觸發數據置換,進而導致性能下降。

    可以使用一致性hash環來保證服務器緩存的數據量基本相同,大致邏輯如下:

    • 將0~2^32個點均勻分配到一個圓上
    • 每個點對應一台緩存服務器
    • 緩存服務器數量是遠小於2^32個的,所以多個節點對應一台緩存服務器,多出來的節點稱為虛擬節點
    • 確保緩存服務器的分布均勻
    • 同樣是對key進行hash
    • 對2^32進行求余
    • 結果對應到hash環上
    • 如果正好落到節點上,則數據就緩存到對應的緩存服務器上
    • 否則就存到落點前面的那個節點所對應的緩存服務器上

    無處不在的緩存

    上面聊的主要是應用緩存,實際上,緩存無處不在。

    下面通過我們訪問網站的流程,來簡單梳理一下,整個過程中,哪些地方可能會用到緩存。

    網絡緩存

    當我們在瀏覽器中輸入URL,按下回車后。

    首先,需要查找域名所對應的IP!這里就有各種緩存!

    • 瀏覽器緩存:瀏覽器會緩存DNS記錄一段時間,首先會從瀏覽器緩存里去找對應的IP。
    • 系統緩存:如果在瀏覽器緩存里沒有找到需要的記錄,就會到系統緩存中查找記錄
    • 路由器緩存:如果系統緩存中也沒找到,就會到路由器緩存中查找記錄
    • ISP DNS 緩存:如果還是找不到,就到ISP緩存DNS的服務器里查找。在這一般都能找到相應的緩存記錄。
    • 遞歸搜索:如果上面的緩存都找不到,就需要從根域名服務器開始遞歸查找了

    找到IP后,還不一定要發請求,因為你訪問的資源可能之前已經訪問過,已經被緩存到了瀏覽器緩存中。此時,瀏覽器直接返回緩存,而不會發送請求。

    如果沒有緩存,則發送請求獲取資源。

    后面可能會達到CDN。CDN是一種邊緣緩存。在用戶訪問網站時,利用GSLB(Global Server Load Balance,全局負載均衡)技術將用戶的訪問指向距離最近的工作正常的緩存服務器上,由緩存服務器直接響應用戶請求。如果CDN中找不到需要的資源,則請求可能就到了反向代理。

    某些反向代理能夠做到和用戶來自同一個網絡,那么用戶訪問反向代理服務器的時候,就會得到很高質量的響應速度,這樣的反向代理緩存一般稱為邊緣緩存,而CDN在邊緣緩存的基礎上,使用了GSLB

    一般反向代理有兩個功能:

    • 隱藏源服務器,防止服務器惡意攻擊。客戶端感知不到代理服務器和源服務器的區別
    • 緩存,將原始服務器數據進行緩存,減少源服務器的訪問壓力

    如果反向代理中也找不到需要的資源,請求才到達源服務器來獲取資源。

    服務端與數據庫緩存

    一般情況下,Server接收到請求后,會根據請求,組裝出響應,進行返回。這個過程可能需要查詢數據庫、進行業務邏輯計算、頁面渲染等操作。這里的每一步都可以引入緩存。

    對於數據庫查詢來說,目前一般的持久化框架都會提供查詢緩存。即對於相同的sql,第二次查詢開始,可以不用再查詢數據庫,直接從緩存中獲取第一次查詢所返回的數據。節省了調用數據庫查詢的時間消耗。對於某些訪問量很大的數據,也可以將其緩存到緩存中間件中。后續直接從緩存中間件中獲取。

    而數據庫本身也有緩存!

    • 客戶端發送一條查詢給服務端
    • 服務端檢查查詢緩存,如果命中緩存,則立刻返回緩存中的結果。如果沒找到,則
    • 進行sql解析、預處理、再由優化器生成對應的執行計划
    • 根據執行計划,調用存儲引擎的API執行查詢
    • 將結果返回給客戶端

    mysql的查詢緩存可能會降低效率。首先,寫緩存是獨占模式寫入。其次,假設一個查詢結果被緩存了,當涉及到的其中一張表數據更新,該緩存都會被置為無效。對於頻繁修改的數據,使用緩存就會降低效率。

    對於業務邏輯計算來說,如果某些業務邏輯很復雜,那么可以針對結果進行緩存。可以將結果緩存到數據庫或緩存中間件中。對於相同的參數的請求,第二次請求時,就不必進行計算,直接從緩存中返回結果即可。

    對於頁面渲染來說,某些訪問量很大的頁面,且數據基本不變的情況下,可以對頁面進行靜態化。即生成靜態的頁面,不必每次訪問的時候都動態生成頁面進行返回,而是預先生成好頁面,將其存到磁盤上,當訪問該頁面的時候,直接從磁盤獲取頁面進行返回即可。或者直接將頁面內容緩存到緩存中間件中,進一步提高性能。

    另外,對於需要登錄的Server來說,用戶信息其實也是緩存下來的。不論是存到服務器Session中,還是存到了緩存中間件中。否則,每次用戶訪問Server都需要到數據庫獲取用戶信息,會影響Server端性能!

    計算機緩存

    最后,運行系統的計算機本身也有很多的緩存!

    我們都知道,一般計算機由CPU、內存、主板、硬盤、顯卡、顯示器、鼠標、鍵盤、網卡等組成!其中存儲類設備包括了:雲存儲(例如:百度雲盤,NAS等)、本地硬盤、內存、CPU中的高速緩存(我們常說的一級緩存、二級緩存和三級緩存)以及CPU寄存器。它們的速度各異,差異達數個量級。下圖顯示了各個設備的訪問速率。

    ![非功能性約束之性能(1)-性能銀彈:緩存](https://p1-tt.byteimg.com/origin/pgc-image/d6c8ccf82f6f49fd9fb04ae7750c181c?from=pc)
    • CPU寄存器最快,達到1ns,但只能存儲幾百個字節,造價也最貴
    • 高速緩存次之,也達到了10ns,可存儲幾十兆,造價次之。其中L1,L2,L3速度越來越慢。
    • 然后是內存,為100ns,可達GB級別,造價比緩存便宜(不過這兩年的內存價格貴得離譜)
    • 硬盤訪問速率為10ms級別,可達TB級別,造價可以說是白菜價了
    • 而雲存儲則達到了秒級,基本可以無限擴展,只要錢夠

    我們都知道CPU的高速緩存是「緩存」,實際上上面的設備,上層設備都可以說是下層設備的「緩存」!

    在《深入理解計算機》一書中,簡單的介紹了計算機執行C語言的hello world程序時的計算機流程。

    • 通過鼠標、鍵盤輸入執行命令'./hello'
    • 輸入的內容從鍵盤通過總線,進入寄存器,在進入內存
    • 當按下回車后
    • 通過DMA技術,將目標文件,從硬盤中直接讀取到內存中
    • 最后執行程序
    • 將hello world拷貝到寄存器
    • 再從寄存器拷貝到顯示器顯示

    可以看到,絕大部分的操作,都是數據的拷貝!最終被CPU執行,為了數據能更快的到達CPU,就有了一層一層的「緩存」!

    • CPU寄存器里的數據是直接給CPU使用的,相當於是L1的緩存
    • L1又是L2的緩存,L2又是L3的緩存
    • L3是內存的緩存
    • 內存又是硬盤的緩存。例如:一般硬盤中的數據,都需要先加載到內存中才能被CPU使用。另外硬盤的“HMB內存緩沖技術”,可以借用內存作為硬盤的緩存。
    • 硬盤本身也是有緩存的,這是為了減少IO操作,批量的進行讀寫。
    • 硬盤也可以是雲存儲的緩存。例如在網絡不太好的情況下,我們可以把電影先下載下來再看,這樣就不會有卡頓的情況

    總結

    性能是架構設計時需要着重考慮的一個非功能性約束,而引入緩存是提高系統性能的一個簡單且直接的方法。

    本文從一個簡單的偽代碼開始,簡單闡述了,緩存的作用,涉及的技術以及目前緩存的使用場景,以期能對架構設計提供一些參考。

    END
    為了更高的交流,歡迎大家關注我的公眾號,掃描下面二維碼即可關注,謝謝:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM