一、從數據說起
我們再做緩存之前需要把數據先分好類
按變化頻率:
- 靜態數據:一般不變的,類似於字典表
- 准靜態數據:變化頻率很低,部門結構設置,全國行政區划數據
- 中間狀態數據:一些計算的可復用中間數據,變量副本,配置中心的本地副本
按使用頻率:
- 熱數據:使用頻率高的
- 讀寫比大的:讀的頻率遠大於寫的頻率
這些數據就比較適合使用緩存。
緩存無處不在。內存可以看作是cpu和磁盤之間的緩存。cpu與內存的處理速度也不一致,所以出現了L1&L2 Cache
緩存的本質:系統各級之間處理速度不匹配,利用空間換時間。
緩存加載時間
1. 啟動時全量加載
2. 懶加載
2.1. 同步使用加載
先看緩存里是否有數據,沒有的話從數據庫讀取。讀取的數據,先放到內存,然后返回給調用方。
2.2. 延遲異步加載
從緩存里獲取數據,不管有沒有都直接返回。
策略1:如果緩存為空的話,則發起一個異步線程負責加載。
策略2:異步線程負責維護緩存的數據,定期或根據條件觸發更新。
緩存過期策略
- 按FIFO或LRU
- 固定時間過期
- 根據業務進行時間的加權。
二、本地緩存
- Map 緩存
public static final Map<String,Object> CACHE=new HashMap();
CACHE.put("key","value");
- Guava緩存
Cache<String,String> cache = CacheBuilder.newBuilder() .maximumSize(1024) .expireAfterWrite(60,TimeUnit.SECONDS) .weakValues() .build();
cache.put("word","Hello Guava Cache");
System.out.println(cache.getIfPresent("word"));
- Spring Cache
- 基於注解和AOP,使用方便
- 可以配置Condition和SPEL,非常靈活
- 需要注意:繞過Spring的話,注解無效
核心功能:@Cacheable、@CachePut、@CacheEvict
本地緩存的缺點:
- 在集群環境中,如果每個節點都保存一份緩存,導致占用內存變大
- 在JVM中長期存在,會影響GC
- 緩存數據的調度處理,影響業務線程,爭奪資源
三、遠程緩存
- Redis
Redis是一個開源的使用ANSI C語言編寫的,基於內存也可以持久化的key-value數據庫,並提供多種語言的API
- Memcached
memcached是一套分布式的高速緩存系統,由LiveJournal的Brad Fitzpatrick開發,但被許多網站使用。這是一套開放源代碼軟件,以BSD license授權發布。
四、內存網格
- Hazelcast
- lgnite
五、緩存常見問題
1. 緩存穿透
問題描述:大量並發查詢不存在的KEY,導致都直接把壓力透傳到數據庫上。
分析:因為數據庫里沒有值,所以沒有建立緩存,導致一直打到數據庫上。
解決辦法:
- 緩存空值的KEY
- Bloom過濾或RoaringBitmap判斷KEY是否存在
- 完全以緩存為准,使用延遲異步加載的方式去加載數據庫數據到緩存。
Bloom過濾器示例:
(引入guava依賴)
public static void main(String[] args) {
BloomFilter<CharSequence> filter = BloomFilter.create(
Funnels.stringFunnel(Charsets.UTF_8),//Funnels.integerFunnel(), //數據格式
1000000,//預計存入數據量
0.01);//誤判率
System.out.println(filter.mightContain("abcdefg"));
filter.put("abcdefg");
System.out.println(filter.mightContain("abcdefg"));
}
RoaringBitmap示例:
引入依賴:
<dependency>
<groupId>org.roaringbitmap</groupId>
<artifactId>RoaringBitmap</artifactId>
<version>0.8.1</version>
</dependency>
public static void test3(){
Roaring64NavigableMap roaring64NavigableMap = Roaring64NavigableMap.bitmapOf(3, 4, 5, 90);
//是否包含
boolean contains = roaring64NavigableMap.contains(3);
long l = roaring64NavigableMap.rankLong(3);
System.out.println(l);
System.out.println(contains);
}
2. 緩存擊穿
問題:當某個KEY失效的時候,正好有大量並發請求訪問這個KEY
分析:跟緩存穿透比較像,這個是屬於偶然的
解決辦法:
- KEY的更新的時候添加全局互斥鎖
- 完全以緩存為准,使用延遲異步加載的策略
3. 緩存雪崩
問題:當某一個時刻發生大規模的緩存失效的情況,會有大量請求打到數據庫,導致數據庫壓力過大而宕機
分析:一般來說,由於更新策略、或者數據熱點、緩存服務宕機等原因,導致緩存數據同時大規模不可以。
解決辦法:
- 緩存更新、失效策略在時間上做到比較均勻
- 使用的熱數據盡量分散到不同機器上
- 多台機器做主從復制,實現高可用
- 實現熔斷限流機制,對系統進行負載能力控制
- 使用本地緩存兜底
4. 先更新數據庫還是先刪除緩存?
4.1 先刪除緩存,再更新數據庫
-
並發情況下:
如果線程A刪除了緩存還沒來得及更新數據庫。那么線程B就會拿不到緩存,從數據庫查詢舊值並寫入緩存。就會導致后來的請求都是從緩存里拿到舊值。 -
異常情況下:如果線程A刪除緩存成功,更新數據庫異常。線程B拿不到緩存也沒關系,從數據庫查,此時數據庫和緩存的值是一致的,沒問題。
4.2 先更新數據庫,再刪除緩存
-
並發情況下:如果線程A更新了數據庫還沒來得及刪除緩存。那么線程B從緩存里取到舊值,但是后續的請求過來緩存已經被刪除了,還是會從數據庫中查詢到新值並放入緩存。
-
異常情況下:如果線程A更新數據庫成功,刪除緩存異常。那么線程B拿到的緩存的值是舊的,有不一致的情況。
綜上,雖然都有不一致的情況,但是第二種更好處理一點。即選擇先更新數據庫,再刪除緩存,模型如下:

如果刪除緩存的時候出了異常,可以將刪除緩存的任務放入消息中間件,讓他重試刪除。
番外:
布隆過濾器:
目標就是要基於過濾器已存儲生成的原始元數據,進行比較過濾,如果是在原始元數據集合里面的,一定會被發現。也有可能不是里面的被誤殺。
BloomFilter 會開辟一個m位的bitArray(位數組),開始所有數據都部署為0,當一個元素過來的時候,通過多個hash函數計算出不同的值,然后根據hash值找到對應的下標處,將里面的值改為1.

優點:使用計算,節省存儲空間。
缺點:有失誤率。不是在過濾器原始表里的數據也會被誤算進去。
使用場景:目標就是要基於過濾器已存儲生成的原始元數據,進行比較過濾,如果是在原始元數據集合里面的,一定會被發現。布隆過濾器核心正確的使用就是進行過濾禁止,進行正確的否定。
舉例:如我們有100萬個黑名單的url地址,過來一個地址我們算出來不在里面,那就肯定可以放行。
BitMap:
BitMap的基本思想是用一個bit位來標記某個元素對應的值,這樣就可以大大節省空間。
在Java中一個int占4個字節,也就是32bit。按int存儲和按位存儲的大小差距是32倍。
那么怎么表示一個數呢?可以使用1表示存在,0表示不存在。
如下面:表示{2,6}

一個byte只有8個位置,如果想表示13怎么辦呢?只能再用一個byte了,就成了一個二維數組了

1個int占32位,那么我們只需要申請一個int數組長度為 int tmp[1+N/32] 即可存儲,其中N表示要存儲的這些數中的最大值
使用場景:
- 快速排序
把數放進去之后,遍歷一遍,把值是1的都取出來就排好序了。
- 快速去重
20億個整數中找出不重復的整數的個數?
內存不足以容納這20億個整數。我們怎么表示數字的狀態呢?一個數的狀態可以分為3種,不存在、存在一次、存在兩次及以上。這就需要兩個bit來表示。00代表不存在,01代表一次,11代表兩次及以上。
接下來我們就把這20億個整數放進去,如果狀態為00,就改為01,如果狀態為01就改為11.如果狀態為11,就不動了。都放完后,遍歷取出值為01的,就是不重復的數據的個數。。
3. 快速查找
給定一個整數M,M/32就能得到int數組的下標,M%32就知道在這個下標里面的具體位置。
如13,就能算出在int[0]里面的第13個
