【總結系列】互聯網服務端技術體系:高性能之緩存面面觀

近水樓臺先得月。html


綜述入口: 互聯網應用服務端的經常使用技術思想與機制綱要java

實際應用中,一些數據在短時間內會反覆屢次訪問。好比循環訪問、熱點暢銷商品、爆熱優惠活動。在一次下單中,提交中的訂單基本信息會被反覆訪問、剛建立的訂單很快會被查詢屢次。算法

數據在短時間內被反覆訪問的場景下,緩存可用來提高查詢性能。緩存是用一個小而快的存儲來存放一個大而慢的存儲的數據子集,在查詢時經過緩存命中而提高性能。緩存是最基本的計算思想之一。在計算機系統的各個層次結構上,緩存無處不在。數據庫

  • CPU 高速緩存:位於 CPU 芯片上。L1,L2,L3 緩存。 L1 - 4 個時鐘;L2 - 10 個時鐘;L3 - 50 個時鐘。
  • 虛擬主存: 做爲磁盤數據的緩存。
  • 磁盤緩存: 難以裝進主存的大對象、網絡內容的本地緩存
  • 網絡緩存: 瀏覽器緩存、HTTP 代理緩存、負載均衡緩存、CDN。

本文總結互聯網技術體系中尤其重要的緩存技術。

數組

基本思想

  • 緩存是以空間換時間,提高查詢性能。緩存遵循「近水樓臺先得月」法則:鄰近 CPU 優先,鄰近用戶優先(CDN)。
  • 緩存依據:訪問局部性原理。時間局部性 - 某個存儲器位置在短期內被再次訪問;空間局部性 - 若某個存儲器位置被訪問,則鄰近存儲器位置也極可能會被訪問。重複引用相同變量的程序具備良好的時間局部性。步長爲 1 的引用模式的程序具備良好的空間局部性。一個典型例子是數組求和。求和變量體現了時間局部性,數組訪問體現了空間局部性。能夠用緩存命中率來衡量局部性。
  • 順序引用模式:順序地每隔 k 個元素地訪問一個連續向量中的每一個元素,稱爲步長爲 k 的順序引用模式 。 k 越大,空間局部性越差。步長爲 1 的順序引用模式是局部性原理的重要應用之一。高效訪問順序與存儲結構設計及存儲細節是緊密關聯的。數組和列表是連續存儲結構,所以順序引用模式很吃香。
  • 存儲器層次結構:對於每一個 k, 位於第 k 層的更快更小的存儲設備做爲位於第 k+1 層的更慢更大的存儲設備的緩存。數據老是以塊爲傳送單元,在第 k 層和第 k+1 層之間進行復制的。層次結構中,相鄰的兩層的塊大小是同樣的;不一樣層次的塊大小能夠不一樣。越靠近慢而大的存儲層次,塊大小越大。

緩存問題

緩存問題主要包括緩存結構設計、緩存一致性分析、緩存策略(熱身/替換/清理)、緩存保護(擊穿/雪崩/穿透)。 一致性問題涉及準確性;緩存策略涉及性能(緩存命中率及主存佔用);而緩存保護涉及穩定性(在大併發請求下且緩存未能命中時保護原始數據源不被壓倒)。

瀏覽器

緩存結構設計

緩存數據結構主要包括記錄型和哈希型。記錄型的緩存,是一個連續存儲陣列,可簡化爲多維數組;哈希型的緩存,是基於哈希表。 CPU 高速緩存是基於記錄型的,由於硬件上不宜作複雜的運算;應用緩存一般是基於哈希型的,好比 Redis 緩存。

緩存

CPU高速緩存

CPU 高速緩存可以使用 (S, E, B, m) 來表示組織結構。m 位存儲器具備 2^m 個存儲器地址,其對應的高速緩存組織劃分爲 S = 2^s 個組,每組 E 個緩存行,每一個緩存行包括一個有效位、t 個標記位、B = 2^b 個字節,緩存大小 C = S * E * B。 其中 s 是組索引,標識緩存塊在哪一個組裏;t = m-s-b 標識緩存塊在緩存組的哪一個緩存行裏;b 是字節在緩存行裏的偏移量。[s,t,b] 標識了緩存字節在緩存結構裏的位置。發生緩存替換時,替換的是某個組裏的某個緩存行。網絡

E = 1 時,DMC Directed-Map Cache ;1 < E < C/B 時,SAC Set Associative Cache ;E = C/B 時 Full Associative Cache FAC。 DMC 每組只有一個緩存行,在組中查找緩存行沒有開銷,但容易發生組的衝突不命中; SAC 在組中查找緩存行有必定開銷,但能夠減小組的衝突不命中機率; FAC 只有一個組,在定位組時無開銷,替換緩存行時有更大的選擇,但在查找緩存行時開銷比較大。在硬件層,搜索和匹配標記位是昂貴的操做,所以 FAC 通常應用在搜索和匹配操做代價不高的地方,好比虛擬主存或應用緩存。數據結構

高速緩存定位字的步驟是:首先從 m 中拿到 s 位組索引,找到緩存行所在的組;再根據 t 位標記位找到匹配的組內的緩存行;最後,根據 b 位偏移量找到字在緩存塊中的位置。若是有效位未置位,則多是過時緩存;若是 t 位標記位沒法匹配全部的組,則是緩存未命中。併發

CPU 寫主存時可採用兩種方式:直寫和回寫。直寫會在更新緩存是直接寫入緩存,而回寫在更新緩存時只是標記緩存塊的緩存狀態,只有在替換緩存塊時纔會寫回主存。這就致使了 CPU 緩存與主存的一致性問題。這個問題是經過 MESI 協議來解決的。


MESI協議

MESI 協議是 SMP 體系結構的 CPU 緩存一致性協議,涉及讀寫時多個 CPU 高速緩存如何與主存保持一致 。主要設計思想包括:緩存條目狀態的狀態轉換自動機、寫緩衝器、總線事務定義及緩存控制、操做異步化隊列、操做屏障。

一致性概念

多處理器存儲系統是一致的,若是某個程序的任何執行結果都知足下列條件:對於任何單元,有可能創建一個假想的操做序列(將全部進程的讀寫操做排成一個全序),此序列與執行結果一致,而且在此序列中:

  • 任何特定進程發出的操做,所表現出的序和該進程向存儲系統發出他們的序相同;
  • 每一個讀操做返回的值是對相應單元按串行順序寫入的最後一個值。

一致性前提

  • 系統總線上的全部事務對全部處理器的高速緩存控制器可見,且以相同順序可見。
  • 爲響應存儲的全部必要事務都出如今總線上,且緩存控制器採起適當的措施。
  • 當高速緩存監聽到與之相關的寫操做事務時,要麼使緩存塊拷貝做廢,要麼更新它。處理器隨後的訪問,要麼緩存不命中而加載新的值,要麼直接看到新的值。

CPU宏觀結構

CPU 宏觀結構主要包括:CPU Core, Store Buffer , CPU Cache , System BUS 。 CPU Cache 和 Store Buffer 是 CPU 專有的,System BUS 是共享的消息通道。 CPU Cache 是一個緩存條目的陣列(多維數組),每一個緩存條目有 tag, data, flag 三個值,tag 表示主存地址,flag 表示緩存條目的狀態。flag 定義了以下值:

  • Modified(M):已修改狀態。某個處理器緩存副本擁有已修改的值, 主存裏的是過時的;
  • Exclusive(E):乾淨獨佔狀態。僅有該處理器緩存副本與主存一致且主存狀態是最新的,獨佔控制權,緩存可以寫操做並轉移到 M 狀態,卻不產生總線事務;
  • Shared(S):至少兩個處理器緩存副本與主存一致,主存有最新的值,其餘處理器可能有最新的或者過時的值;
  • Invalid(I):初始狀態,緩存無效狀態。

緩存條目狀態簡稱爲 CES。CES 的狀態轉換圖能夠定位爲一個有限狀態自動機。理解 CES 的有限狀態轉換機是關鍵。以下圖所示,A/B 表示當觀察 A 事件時,將產生一個 B 總線事務。Flush’ 表示清除相應的存儲塊,前提是使用了緩存到緩存的共享,且清除是由提供數據的緩存。BusRd(S) 表示由共享信號 S 生成的總線讀事務。緩存控制器經過共享信號 S 在地址階段肯定是否有其它緩存擁有一樣的緩存拷貝。若是一個緩存肯定本身擁有一樣的存儲塊拷貝,就會發出 S 信號。

MESI 協議定義了一些總線事務(總線讀事務、總線排它讀事務、總線寫事務、回寫事務)。結合 CES 狀態轉換圖、總線事務及 CPU 緩存讀寫控制來實現一致性。

緩存讀

讀是指拿到變量的最新值並讀取到 CPU 寄存器。假設處理器 P1 和 P2 均擁有變量 x 的副本。若是 P1 發現 x 的 CES 爲 M/E/S,則直接獲取副本 x 的值。若 P1 發現變量 x 的 CES 爲 I,則遵循以下步驟:

  • STEP1 -- 發送 BUS Read 事務;
  • STEP2 -- P2 擁有變量 x 的最新副本( CES 爲 M),嗅探到 x Read 事務,就會將 x 的最新副本寫入主存,構造 Read Response 發送到 BUS 上,並將 CES 更新爲 S ;若是有多個處理器緩存都擁有變量 x 的最新副本,則經過某種策略來選擇從某個高速緩存來提供新值仍是直接由主存來提供新值。
  • STEP3 -- P1 嗅探到到 x Read Response ,將 CES 更新爲 S,寫入相應的緩存塊。

注意:任何一個處理器在嗅探到緩存塊的 BUS Read 事務,且相應緩存塊爲 M 狀態時,都會執行 STEP2 操做。

緩存寫

寫是指將變量 x 的最新值寫到緩存塊。對一個處於 E 或 I 狀態的緩存塊的寫操做,將其置爲 M 狀態以前,全部其餘處理器緩存拷貝都必須經過一個排它讀總線事務將本身的緩存做廢。若是緩存狀態是 M/E ,則不發送總線事務;遵循以下步驟:

  • STEP1:P1 發送總線排它讀事務;
  • STEP2:其餘處理器嗅探到總線排它讀事務,更新 CES 爲 I,再發送 Invalidate Acknowledge ;後續讀會產生一次緩存不命中,從而經過一次總線讀事務讀取最新值。
  • STEP3:P1 收到全部 Invalidate Acknowledge ,將 CES 更新爲 E,得到數據控制權。而後寫入緩存行,將 CES 更新爲 M。CPU 寫須要等待其餘處理器都發送 Invalidate Acknowledge 消息,此時會有寫等待問題。

緩存替換

當一個緩存塊被替換時:

  • 若是緩存塊處於 S 或 I, 則邏輯上直接更新爲 I; 若是緩存塊處於 M 狀態,則從 M 到 I 的狀態轉換會觸發一次回寫事務,將緩存塊的狀態寫入主存。

寫等待問題

寫緩衝器(Store Buffer)、無效化隊列(Invalidate Queue)。CPU 會直接先寫 Store Buffer ,再同步緩存。其餘處理器則會將消息存入 Invalidate Queue 就發送 Invalidate Acknowledge ,異步去更新 CES 。 寫緩衝器和無效化隊列將 CPU 緩存副本更新變成異步處理。讀則採用存儲轉發,先查詢寫緩衝器,再查詢高速緩存。至關於寫緩衝器又加了一層緩存。寫緩存異步化又會帶來一致性問題。

主存屏障

Store Barrier 和 Load Barrier 。Store Barrier 將 Store Buffer 的數據寫入緩存; Load Barrier 根據 Invalidate Queue 的主存地址,將相應的 CES 更新爲 I。

緩存讀寫一致性

要正確使用緩存,必然要保證緩存併發讀寫的一致性。緩存讀寫一致性須要保證:

  • 源數據與緩存的同一數據達成一致性(或者達成不一致時延可接受的最終一致性)。
  • 以源數據的數據爲準。

能夠採用 [ xC, xDB, yC, yDB ] 操做序列分析讀寫一致性問題,x,y 是讀、更新、刪除,C 表示緩存,DB 表示數據庫(源數據)。

首先框定討論範圍:兩個線程 A, B,一個變量 x ,數據源 DB 和 緩存 C ,其中 C 從 DB 中獲取,須要與 DB 保持一致, A,B 有讀寫操做,讀爲 RD, 寫能夠進一步分爲更新值 UP 和刪除值操做 DE,讀寫時序不肯定。

緩存讀模式是肯定的:讀取數據時,先讀緩存,緩存命中則直接返回(查詢性能提高體如今這裏),未命中再去讀 DB。這點無異議。若是 A, B 併發讀,均直接從 C 中獲取當前值便可。若是 C 中沒有值,那麼 A, B 可能都會從 DB 獲取。在大併發的情形下,會有緩存擊穿/穿透的問題。緩存擊穿和穿透的問題在後面討論。

當兩個線程處於併發讀-併發寫,或者併發寫-併發寫的時候,能夠有兩種方案:加鎖和不加鎖。

  • 對更新 DB 和更新 C 進行加分佈式鎖,使之原子化。加鎖會更簡單,但吞吐量會比較低;
  • 對更新 DB 和 更新 C 不加鎖,保證合理的執行順序,使之達到最終一致性(業務可接受必定的不一致時延)。

如下主要討論不加鎖的方案。分情形討論:

A寫-B讀

先指明指望結果:

  • 若是 B 在 A 寫以前讀,那麼 B 讀到的是寫以前的值,直接從 C 中獲取便可。沒問題。
  • 若是 B 在 A 寫以後讀,那麼 B 讀到的應該是寫以後的值。

那麼 A 該如何寫,才能保證 B 讀到最新的值?

  • A 先更新 C,再寫 DB。會有什麼問題 ? 若是 A 先更新了 C ,而後更新 DB 失敗了,那麼 C 與 DB 就不一致了。DB 裏是舊值。不符合一致性保證。固然,讀 C 會讀到新值。能夠有一種思路,就是緩存做爲讀寫的前置環節,負責緩存與 DB 的最終一致性。這種方案也是能夠接受的。這是 Write Behind Caching Pattern 模式。
  • A 先刪除 C, 再寫 DB。會有什麼問題? 若是操做時序是 [ A 刪除 C, B 讀 C,A 更新 DB ], 那麼在 A 更新 DB 以前,B 讀 C 未命中,從 DB 讀到舊值,而 A 更新 DB, DB 與 C 的值不一致。不符合一致性保證。
  • A 先寫 DB ,再更新 C。會有什麼問題? 1. 更新緩存多是代價昂貴的操做,頻繁更新緩存會致使吞吐量下降;在更新 C 以前讀到的是舊值,若是更新操做比較耗時,那麼 DB 和 C 的不一致時延會比較大,會影響業務。
  • A 先寫 DB ,再刪除 C。 會有什麼問題? 避免了更新代價高的問題,在刪除 C 以前讀到的是舊值,在刪除 C 以後讀到新值。因爲刪除緩存每每代價很小,不一致時延一般可接受。這是 Cache Aside Pattern 模式。

A寫-B寫

  • 指望結果:不管 A 先寫仍是 B 先寫,最終寫入的 DB 和 C 的值應當一致。
  • A 先更新 C,再寫 DB。 會有什麼問題?若是操做時序是 [ A 寫 C x, B 寫 C y, B 寫 DB y, A 寫 DB x],會致使 C 是 y , DB 是 x ,DB 與 C 中的數據不一致。
  • A 先寫 DB, 再更新 C。會有什麼問題?[ A 寫 DB x, B 寫 DB y, B 寫 C y, A 寫 C x],會致使 C 是 x , DB 是 y ,DB 與 C 中的數據不一致。

從上述分析可知:1. 更新緩存操做多是一個代價昂貴的操做,會致使 DB 與 C 達到最終一致性的不一致時延較長,對業務有影響; 2. 在併發寫-寫模式下,DB 和 C 的數據會不一致,從而讀到不一致的數據。所以,通常不採用更新緩存的方式,而是直接刪除緩存。

常見的緩存讀寫模式有 Cache Aside Pattern 和 Write Behind Caching Pattern 。

  • Cache Aside Pattern:讀取數據時,先讀緩存,緩存命中則直接返回(查詢性能提高體如今這裏),未命中再去讀 DB。讀更寫刪。讀模式基本是固定的;寫入數據時,先更新 DB ,再刪除緩存。能夠採集 DB binlog 異步刪除緩存。若是是主從 DB,則必須採集最後一個從庫 binlog (最終一致性)。
  • Write Behind Caching Pattern --- 寫入時只更新緩存,異步去更新 DB 。犧牲短暫的一致性來得到高吞吐量。

緩存熱身

空緩存會直接致使不命中,從而影響第一次讀的性能。若是大併發訪問空緩存(相似緩存雪崩),很容易致使大量併發請求直接打到 DB 上,使得 DB 壓力陡增。

緩存熱身便是預先把一些數據加載到緩存,提高第一次訪問的性能,同時防止第一次訪問面臨大併發時會將後臺打出問題。好比在應用啓動後,能夠將一些 TOPN 商品異步加載到緩存(不能影響應用啓動);商家作活動前,把一些活動商品和活動信息數據加載到緩存(可配置化);把一些極少變更的靜態數據加載到緩存。加載緩存可使用應用通知機制,好比實現 ApplicationListener 的
onApplicationEvent 方法。

緩存替換策略

緩存總有未命中的狀況:

  • 空不命中:老是不會命中,亦稱冷緩存。避免冷緩存的方法是進行「緩存熱身」。將 k+1 層的緩存塊放到第 k 層的策略稱爲放置策略。一般採用取模的方式: j = i Mod N ,即:將第 k+1 層的第 i 個塊對 N 取模後,放到第 k 層的第 i 個塊裏。
  • 衝突不命中:好比按取模的放置策略,有可能在緩存未滿的狀況下,老是對第 k 層的同一個塊進行替換。好比 j mod 4 ,當 j=0,4,8,12 時,老是會放在到第 0 塊上。緩存抖動是一種特殊的衝突不命中,指高速緩存反覆加載或驅逐相同的高速緩存塊/組/行。
  • 容量不命中:緩存容量滿了。

緩存替換策略是指當緩存未命中,且緩存容量已滿時,判斷要替換哪一個塊的緩存數據。原則上,應該淘汰:1. 只訪問過一次的數據; 2. 相比其餘數據更少訪問的; 3. 在一段時間內沒有再訪問的。

緩存替換策略主要有 FIFO, LRU, LFU。

  • FIFO : 最早進入緩存的首先被淘汰。隊列實現。或者使用雙向鏈表,新進入元素添加到鏈表尾,丟棄鏈表頭的元素。FIFO適合丟棄那些只有一次訪問的數據。
  • LRU :最近最少使用淘汰。使用鏈表實現,若緩存命中,則將節點移至首部,淘汰尾部節點。 LRU 適合熱點數據訪問。LRU 沒法識別哪些緩存是最多被訪問的。偶發性、週期性的批量操做可能致使緩存被大量替換,形成緩存污染,使得 LRU 的效率大幅降低。實際採用 LRU-K 算法,將緩存分爲兩級,數據在較短期被訪問 K 次以上,則進入二級緩存。兩級都採用 LRU 策略。
  • LFU : 最少次數使用淘汰。引用計數 + 優先級隊列(堆)。

緩存清理策略

當緩存對應的原始數據更新後,緩存裏的數據就與原始數據不一致了,即緩存失效了。這時候須要及時清理緩存,避免讀到過時數據以及過時數據佔用過大的內存。緩存清理策略是指何時清理過時或失效緩存。

  • TTL: 設置過時時間。 TTL 通常以數據變化頻繁度爲依據來設置。不一樣業務數據的過時時間不同。
  • 寫時失效: 寫失效、寫更新。寫失效 - 標記緩存數據已過時,讀時清理或替換;寫更新 - 在更新數據時就替換緩存項。
  • 讀時失效:寫時只標註失效信息,讀時判斷是否失效並加載最新數據。若是有大量緩存對象要更新,能夠採用讀時失效將寫更新成本分攤到每個讀上。緩存對象時,同時存儲相應的版本號或時間戳。須要展現數據時,經過對比版本號來判斷是否緩存已失效。

緩存擊穿/雪崩/穿透

  • 緩存擊穿【重點】。 熱點問題。大併發集中對熱點 key 進行訪問,當這個 key 在失效的瞬間,持續的大併發就穿破緩存,直接請求數據庫,就像在一個屏障上鑿開了一個洞。基本方案:多級緩存(不一樣失效時間)+ 熱點散列 + 熱點識別、熔斷降級、互斥鎖、不過時+異步更新。
  • 緩存雪崩。 大量 key 同時失效,致使大量請求打到 DB,形成巨大 DB 壓力和系統不穩定。基本方案:過時時間+隨機化。
  • 緩存穿透。大量不存在的 key 的非法訪問請求,一樣會使得大量請求打到 DB。使用布隆過濾器過濾大量非法請求。還有一種方法是空值緩存,失效時間設置小一些,應對短期內無效重複 key 的大量查詢。
  • 緩存命中統計、緩存監控。

緩存實現

以本地緩存爲例,來分析緩存實現。本地緩存一般在單機共享範圍內:某個進程內的被屢次訪問的主存數據;單機範圍內的多進程共享的主存數據。要實現緩存功能,一般須要考慮以下因素:

  • 緩存的規格指定,會影響緩存的建立和性能。
  • 緩存的值的計算和遲加載。
  • 緩存策略的配置。
  • 緩存對併發的支持。
  • 緩存更新的通知與監聽。
  • 緩存的監控與統計。

Guava.Cache 是本地緩存的一個實現。核心類是 CacheBuilderSpec (規格指定)、CacheBuilder (根據緩存規格建立緩存)、LocalCache (緩存功能的核心實現類)。 LocalCache 的底層是一個哈希表,支持併發訪問,實現了 ConcurrentMap 接口。實現要點以下:

  • 緩存數據的讀寫與 ConcurrentHashMap 相似。
  • 有兩個用雙向鏈表實現的優先級隊列: writeQueue 和 accessQueue ,用來控制緩存什麼時候過時。writeQueue 按寫時間排序,accessQueue 按訪問時間排序。在每次寫入或更新或清理操做的時候,會執行清理操做,根據這兩個隊列來判斷緩存數據是否過時,若是過時則從緩存數據哈希表中移除。

高效應用緩存

緩存友好的代碼

針對連續型存儲的高速緩存,編寫對緩存友好的代碼。好比聚焦核心函數的循環;減小循環內部不命中的數量;對局部變量的反覆引用;步長爲 1 的順序引用模式;多重循環中的循環變量的次序。

換言之,每一個循環都會在高速緩存上產生很大的影響,進而影響程序運行性能。對於上層應用可能感知不明顯,可是對於底層卻很重要。


緩存與動態規劃

動態規劃法一般會複用到子問題的解,所以可使用緩存來存儲子問題的解。一個簡單的例子以下,計算階乘:

public class factorialCalc {

    private static Log log = LogFactory.getLog(factorialCalc.class);

    static Random random = new Random(System.currentTimeMillis());

    public static void main(String[]args) {

        for (int i=1; i < 10; i++) {
            int num = random.nextInt(15);
            String info = String.format("fac(%d)=%d", num, fac(num));
            log.info(info);

            String info2 = String.format("facWithCache(%d)=%d", num, facWithCache(num));
            log.info(info2);
            printCacheInfo(cache);
        }
    }

    private static void printCacheInfo(Cache<Integer, Long> cache) {
        log.info("cache contents: " + cache.asMap());
        log.info("cache stat: " + cache.stats());
    }

    public static long fac(int n) {
        if (n <= 1) return 1;
        return n * fac(n-1);
    }

    private static Cache<Integer, Long> cache = CacheBuilder.newBuilder().recordStats().build();

    public static long facWithCache(int n) {
        if (n <= 1) {
            cache.put(1, 1L);
            return 1L;
        }
        Long facN_1 = cache.getIfPresent(n-1);
        if (facN_1 == null) {
            facN_1 = facWithCache(n-1);
        }
        long facN = n * facN_1;
        cache.put(n, facN);
        return facN;
    }
}

分佈式緩存

通常採用 Redis 來作多機共享的分佈式緩存。一些有效作法:

  • 命名空間規範和隔離,部署隔離,避免業務相互影響和耦合。
  • 採用批量獲取緩存數據的方法提高查詢性能,減小網絡傳輸開銷。
  • 儘可能使用 O(1) 的命令,避免使用遍歷性命令。
  • 單個 key 的 value 不超過 10KB, list, set, map 等不超過 1000 個元素。
  • 設置合理的按期刪除/惰性刪除/緩存替換策略。
  • 測量緩存的命中率及性能提高狀況;若數據不理想,則要仔細分析緣由並優化。
  • 監控大對象緩存。

要避免的坑:

  • 內存佔用和緩存同步要特別注意,避免內存佔用大、同步慢影響了業務。
  • 緩存主要用來提高性能,不要當作持久化存儲使用,避免數據丟失的風險。
  • 避免濫用和浪費緩存資源。主存緩存是比較昂貴的資源。

參考資料

相關文章
相關標籤/搜索