近水樓臺先得月。html
綜述入口: 互聯網應用服務端的經常使用技術思想與機制綱要java
實際應用中,一些數據在短時間內會反覆屢次訪問。好比循環訪問、熱點暢銷商品、爆熱優惠活動。在一次下單中,提交中的訂單基本信息會被反覆訪問、剛建立的訂單很快會被查詢屢次。算法
數據在短時間內被反覆訪問的場景下,緩存可用來提高查詢性能。緩存是用一個小而快的存儲來存放一個大而慢的存儲的數據子集,在查詢時經過緩存命中而提高性能。緩存是最基本的計算思想之一。在計算機系統的各個層次結構上,緩存無處不在。數據庫
本文總結互聯網技術體系中尤其重要的緩存技術。
數組
緩存問題主要包括緩存結構設計、緩存一致性分析、緩存策略(熱身/替換/清理)、緩存保護(擊穿/雪崩/穿透)。 一致性問題涉及準確性;緩存策略涉及性能(緩存命中率及主存佔用);而緩存保護涉及穩定性(在大併發請求下且緩存未能命中時保護原始數據源不被壓倒)。
瀏覽器
緩存數據結構主要包括記錄型和哈希型。記錄型的緩存,是一個連續存儲陣列,可簡化爲多維數組;哈希型的緩存,是基於哈希表。 CPU 高速緩存是基於記錄型的,由於硬件上不宜作複雜的運算;應用緩存一般是基於哈希型的,好比 Redis 緩存。
緩存
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 協議是 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 定義了以下值:
緩存條目狀態簡稱爲 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,則遵循以下步驟:
注意:任何一個處理器在嗅探到緩存塊的 BUS Read 事務,且相應緩存塊爲 M 狀態時,都會執行 STEP2 操做。
緩存寫
寫是指將變量 x 的最新值寫到緩存塊。對一個處於 E 或 I 狀態的緩存塊的寫操做,將其置爲 M 狀態以前,全部其餘處理器緩存拷貝都必須經過一個排它讀總線事務將本身的緩存做廢。若是緩存狀態是 M/E ,則不發送總線事務;遵循以下步驟:
緩存替換
當一個緩存塊被替換時:
寫等待問題
寫緩衝器(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 獲取。在大併發的情形下,會有緩存擊穿/穿透的問題。緩存擊穿和穿透的問題在後面討論。
當兩個線程處於併發讀-併發寫,或者併發寫-併發寫的時候,能夠有兩種方案:加鎖和不加鎖。
如下主要討論不加鎖的方案。分情形討論:
A寫-B讀
先指明指望結果:
那麼 A 該如何寫,才能保證 B 讀到最新的值?
A寫-B寫
從上述分析可知:1. 更新緩存操做多是一個代價昂貴的操做,會致使 DB 與 C 達到最終一致性的不一致時延較長,對業務有影響; 2. 在併發寫-寫模式下,DB 和 C 的數據會不一致,從而讀到不一致的數據。所以,通常不採用更新緩存的方式,而是直接刪除緩存。
常見的緩存讀寫模式有 Cache Aside Pattern 和 Write Behind Caching Pattern 。
空緩存會直接致使不命中,從而影響第一次讀的性能。若是大併發訪問空緩存(相似緩存雪崩),很容易致使大量併發請求直接打到 DB 上,使得 DB 壓力陡增。
緩存熱身便是預先把一些數據加載到緩存,提高第一次訪問的性能,同時防止第一次訪問面臨大併發時會將後臺打出問題。好比在應用啓動後,能夠將一些 TOPN 商品異步加載到緩存(不能影響應用啓動);商家作活動前,把一些活動商品和活動信息數據加載到緩存(可配置化);把一些極少變更的靜態數據加載到緩存。加載緩存可使用應用通知機制,好比實現 ApplicationListener 的
onApplicationEvent 方法。
緩存總有未命中的狀況:
緩存替換策略是指當緩存未命中,且緩存容量已滿時,判斷要替換哪一個塊的緩存數據。原則上,應該淘汰:1. 只訪問過一次的數據; 2. 相比其餘數據更少訪問的; 3. 在一段時間內沒有再訪問的。
緩存替換策略主要有 FIFO, LRU, LFU。
當緩存對應的原始數據更新後,緩存裏的數據就與原始數據不一致了,即緩存失效了。這時候須要及時清理緩存,避免讀到過時數據以及過時數據佔用過大的內存。緩存清理策略是指何時清理過時或失效緩存。
以本地緩存爲例,來分析緩存實現。本地緩存一般在單機共享範圍內:某個進程內的被屢次訪問的主存數據;單機範圍內的多進程共享的主存數據。要實現緩存功能,一般須要考慮以下因素:
Guava.Cache 是本地緩存的一個實現。核心類是 CacheBuilderSpec (規格指定)、CacheBuilder (根據緩存規格建立緩存)、LocalCache (緩存功能的核心實現類)。 LocalCache 的底層是一個哈希表,支持併發訪問,實現了 ConcurrentMap 接口。實現要點以下:
緩存友好的代碼
針對連續型存儲的高速緩存,編寫對緩存友好的代碼。好比聚焦核心函數的循環;減小循環內部不命中的數量;對局部變量的反覆引用;步長爲 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 來作多機共享的分佈式緩存。一些有效作法:
要避免的坑: