目錄
一、介紹
二、Caffeine核心原理與架構設計
2.1 存儲引擎與數據結構
2.2 緩存淘汰策略
2.3 并發控制機制
?三、入門案例
3.1 引入依賴
3.2 測試接口
3.3 小結
四、Caffeine常用方法詳解
4.1?getIfPresent
4.2 get
4.3 put
4.4 putAll
4.5 invalidate
4.6 invalidateAll
五、構建一個更加全面的緩存
5.1、容量控制配置
(1)??initialCapacity(int)???
(2)??maximumSize(long)?? ?
(3)??maximumWeight(long)??
5.2、過期策略配置
(1)expireAfterAccess(long, TimeUnit)??
(2)??expireAfterWrite(long, TimeUnit)???
?(3)?expireAfter(Expiry)??
5.3 注意事項
六、整合Spring Cache
6.1 引入依賴
6.2 配置文件
6.3 使用
七、生產環境注意事項
八、實現Caffeine與Redis多級緩存完整策略(待完善)?
一、介紹
JDK內置的Map可作為緩存的一種實現方式,然而嚴格意義來講,其不能算作緩存的范疇。
原因如下:一是其存儲的數據不能主動過期;二是無任何緩存淘汰策略。
Caffeine是一個基于Java 8的高性能本地緩存庫,由Ben Manes開發,旨在提供快速、靈活的緩存解決方案。作為Guava Cache的現代替代品,Caffeine在性能、功能和靈活性方面都有顯著提升。
Caffeine作為Spring體系中內置的緩存之一,Spring Cache同樣提供調用接口支持。已成為Java生態中最受歡迎的本地緩存庫之一。
本文將全面介紹Caffeine的核心原理、使用方法和最佳實踐。
二、Caffeine核心原理與架構設計
2.1 存儲引擎與數據結構
Caffeine底層采用優化的ConcurrentHashMap作為主要存儲結構,并在此基礎上進行了多項創新:
- ??分段存儲技術??:使用StripedBuffer實現無鎖化并發控制,將競爭分散到多個獨立緩沖區,顯著提升并發吞吐量。
- ??頻率統計機制??:采用Count-Min Sketch算法記錄訪問頻率,以93.75%的準確率僅使用少量內存空間。
- ??時間輪管理??:通過TimerWheel數據結構高效管理過期條目,實現納秒級精度的過期控制。
2.2 緩存淘汰策略
Caffeine采用了創新的Window-TinyLFU算法,結合了LRU和LFU的優點:
- ??三區設計??:窗口區(20%)、試用區(1%)和主區(79%),各區使用LRU雙端隊列管理
- ??動態調整??:根據訪問模式自動調整各區比例,最高可實現98%的緩存命中率
- ??頻率衰減??:通過周期性衰減歷史頻率,防止舊熱點數據長期占據緩存
相比Guava Cache的LRU算法,Window-TinyLFU能更準確地識別和保留真正的熱點數據,避免"一次性訪問"污染緩存。
2.3 并發控制機制
Caffeine的并發控制體系設計精妙:
- ??寫緩沖機制??:使用RingBuffer和MpscChunkedArrayQueue實現多生產者-單消費者隊列
- ??樂觀鎖優化??:通過ReadAndWriteCounterRef等自定義原子引用降低CAS開銷
- ??StampedLock應用??:在關鍵路徑上使用Java 8的StampedLock替代傳統鎖,提升并發性能
?三、入門案例
3.1 引入依賴
以springboot 2.3.x為例,
<!-- caffeine -->
<dependency><groupId>com.github.ben-manes.caffeine</groupId><artifactId>caffeine</artifactId>
</dependency>
3.2 測試接口
package com.example.demo;import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.UUID;@RestController
@RequestMapping("/api")
public class Controller {@GetMapping("writeCache")public String writeCache() {Cache<Object, Object> cache = Caffeine.newBuilder().build();cache.put("uuid", UUID.randomUUID());User user = new User("張三", "123456@qq.com", "abc123", 18);cache.put("user", user);return "寫入緩存成功";}@GetMapping("readCache")public String readCache() {Cache<Object, Object> cache = Caffeine.newBuilder().build();Object uuid = cache.getIfPresent("uuid");Object user = cache.getIfPresent("user");return "uuid: " + uuid + ", user: " + user;}}
?
問題:明明調用接口寫入了緩存,為什么我們查詢的時候還是沒有呢?
細心的你可能已經發現了,我們在每個接口都重新構造了一個新的Cache
實例。這兩個Cache
實例是完全獨立的,數據不會自動共享。
解決辦法
所以,聰明的你可能就想著把它提取出來,成功公共變量吧
@RestController
@RequestMapping("/api")
public class Controller {Cache<Object, Object> cache = Caffeine.newBuilder().build();@GetMapping("writeCache")public String writeCache() {cache.put("uuid", UUID.randomUUID());User user = new User("張三", "123456@qq.com", "abc123", 18);cache.put("user", user);return "寫入緩存成功";}@GetMapping("readCache")public String readCache() {Object uuid = cache.getIfPresent("uuid");Object user = cache.getIfPresent("user");return "uuid: " + uuid + ", user: " + user;}}
你看這不就有了!于是聰明的你,又想:“如果放在這個控制器類下面,那我其他類中要是想調用,是不是不太好?”
于是你又把它放在一個配置類下面,用于專門管理緩存。
package com.example.demo;import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class CacheConfig {@Beanpublic Cache<String, Object> buildCache() {return Caffeine.newBuilder().build();}
}
@RestController
@RequestMapping("/api")
public class Controller {@Resourceprivate Cache<String, Object> cache;@GetMapping("writeCache")public String writeCache() {cache.put("uuid", UUID.randomUUID());User user = new User("張三", "123456@qq.com", "abc123", 18);cache.put("user", user);return "寫入緩存成功";}@GetMapping("readCache")public String readCache() {Object uuid = cache.getIfPresent("uuid");Object user = cache.getIfPresent("user");return "uuid: " + uuid + ", user: " + user;}}
聰明的你,發現依然可以呀!真棒!
于是你又靈機一動,多定義幾個bean吧,一個設置有效期,一個永不過期。
@Configuration
public class CacheConfig {@Bean("noLimit")public Cache<String, Object> buildCache() {return Caffeine.newBuilder().build();}@Bean("limited")public Cache<String, Object> buildLimitedCache() {// 設置過期時間是30sreturn Caffeine.newBuilder().expireAfterWrite(30, TimeUnit.SECONDS).build();}
}
@RestController
@RequestMapping("/api")
public class Controller {@Resource(name = "limited")private Cache<String, Object> cache;@GetMapping("writeCache")public String writeCache() {cache.put("uuid", UUID.randomUUID());User user = new User("張三", "123456@qq.com", "abc123", 18);cache.put("user", user);return "寫入緩存成功";}@GetMapping("readCache")public String readCache() {Object uuid = cache.getIfPresent("uuid");Object user = cache.getIfPresent("user");return "uuid: " + uuid + ", user: " + user;}}
你發現30s后加入的緩存也沒有了。
3.3 小結
通過這個案例,你似乎也覺察到了,Caffeine的基本使用方法
- 導入依賴
- 構建公共緩存對象(expireAfterWrite方法可以設置寫入后多久過期)
- 使用?put()?方法添加緩存
- 使用 getIfPresent() 方法讀取緩存
- 一旦重啟項目,緩存就都消失了(基于本地內存)!
四、Caffeine常用方法詳解
4.1?getIfPresent
@Nullable V getIfPresent(@CompatibleWith("K") @NonNull Object var1);
前面已經演示過了,這里就不在舉例了。意思是如果存在則獲取,不存在就是null。
4.2 get
@Nullable V get(@NonNull K var1, @NonNull Function<? super K, ? extends V> var2);
@GetMapping("readCache")
public String readCache() {Object uuid = cache.getIfPresent("uuid");Object user = cache.get("user", item -> {// 緩存不存在時,執行加載邏輯return new User("李四", "456789@qq.com", "def456", 20);});return "uuid: " + uuid + ", user: " + user;
}
4.3 put
?void put(@NonNull K var1, @NonNull V var2);
?入門案例也演示過了,就是添加緩存。使用方法和普通的map類似,都是key,value的形式。
4.4 putAll
void putAll(@NonNull Map<? extends @NonNull K, ? extends @NonNull V> var1);
putAll 顧名思義,就是可以批量寫入緩存。首先定義一個map對象,把要加入的緩存往map里面塞,然后把map作為參數傳遞給這個方法即可。
4.5 invalidate
手動清除單個緩存
cache.invalidate("key1");
4.6 invalidateAll
手動批量清除多個key
// 批量清除多個key
cache.invalidateAll(Arrays.asList("key1", "key2"));
手動清除所有緩存
// 清除所有緩存
cache.invalidateAll();
💡注意:
這些方法會立即從緩存中移除指定的條目。
Caffeine除了手動清除外,也和Redis一樣,有自動清除策略。這些將在下一張集中講解。
五、構建一個更加全面的緩存
前面我們演示時,通過Caffeine.newBuilder().build();就建完了緩存對象,頂多給它設置了一個過期時間。
但是關于這個緩存對象本身,還有很多東西是可以設置的,下面我們就詳細說說,還有哪些設置。
Caffeine.newBuilder() 提供了豐富的配置選項,可以創建高性能、靈活的緩存實例。以下是主要的可配置內容:
5.1、容量控制配置
(1)??initialCapacity(int)???
設置初始緩存容量
示例:.initialCapacity(100)
?表示初始能存儲100個緩存對象
(2)??maximumSize(long)?? ?
按條目數量限制緩存大小
示例:.maximumSize(1000)
?表示最多緩存1000個條目
(3)??maximumWeight(long)??
按自定義權重總和限制緩存大小
需要配合weigher()使用
示例:.maximumWeight(10000).weigher((k,v) -> ((User)v).getSize())
注意:maximumSize和maximumWeight不能同時使用
當緩存條目數超過最大設定值時,Caffeine會根據Window TinyLFU算法自動清除"最不常用"的條目
5.2、過期策略配置
(1)expireAfterAccess(long, TimeUnit)??
設置最后訪問后過期時間
示例:.expireAfterAccess(5, TimeUnit.MINUTES)
(2)??expireAfterWrite(long, TimeUnit)???
設置創建/更新后過期時間
示例:.expireAfterWrite(10, TimeUnit.MINUTES)
?(3)?expireAfter(Expiry)??
自定義過期策略
可以基于創建、更新、讀取事件分別設置
.expireAfter(new Expiry<String, Object>() {public long expireAfterCreate(String key, Object value, long currentTime) {return TimeUnit.HOURS.toNanos(1); // 創建1小時后過期}public long expireAfterUpdate(String key, Object value, long currentTime, long currentDuration) {return currentDuration; // 保持原過期時間}public long expireAfterRead(String key, Object value, long currentTime, long currentDuration) {return currentDuration; // 保持原過期時間}
})
5.3 注意事項
Caffeine的清除操作通常是異步執行的,如果需要立即清理所有過期條目,可以調用:
cache.cleanUp();
這個方法會觸發一次完整的緩存清理,包括所有符合條件的過期條目。
六、整合Spring Cache
前面介紹時說了,Caffeine作為Spring體系中內置的緩存之一,Spring Cache同樣提供調用接口支持。所以接下來,我們詳細實現整合過程。
6.1 引入依賴
<!-- caffeine -->
<dependency><groupId>com.github.ben-manes.caffeine</groupId><artifactId>caffeine</artifactId>
</dependency><!-- cache -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId>
</dependency>
6.2 配置文件
@Configuration
public class CacheConfig {@Beanpublic CacheManager cacheManager() {CaffeineCacheManager cacheManager = new CaffeineCacheManager();cacheManager.setCaffeine(Caffeine.newBuilder().initialCapacity(100) // 初始容量.maximumSize(500) // 最大緩存條目數.expireAfterWrite(10, TimeUnit.MINUTES) // 寫入后10分鐘過期.expireAfterAccess(5, TimeUnit.MINUTES) // 訪問后5分鐘過期.weakKeys() // 使用弱引用鍵.recordStats()); // 記錄統計信息return cacheManager;}
}
6.3 使用
具體使用方法可以參考前面寫的這篇文章Spring Cache用法很簡單,但你知道這中間的坑嗎?-CSDN博客
springcache無非就是那幾個注解。這里淺淺舉例演示
@RestController
@RequestMapping("/api")
public class Controller {@GetMapping("test")@Cacheable(value = "demo")public User test() {System.out.println("-----------------------");return new User("張三", "123456@qq.com", "abc123", 18);}}
多次刷新,idea控制臺也僅僅打印了一次---------------------------
說明緩存生效了!
七、生產環境注意事項
提到緩存,那就是老生常談的:緩存穿透、緩存擊穿和緩存雪崩等問題。
緩存穿透防護??:
- 對null值進行適當緩存(使用
unless = "#result == null"
) - 考慮使用Bloom過濾器
??緩存雪崩防護??:
- 為不同緩存設置不同的過期時間
- 添加隨機抖動因子到過期時間
??緩存一致性??:
- 重要數據建議配合數據庫事務
- 考慮使用
@CachePut
更新策略
??內存管理??:
- 合理設置
maximumSize
防止OOM - 對大對象考慮使用
weakValues()
或softValues()
??分布式環境??:
- 本地緩存需要配合消息總線實現多節點同步
- 或考慮使用多級緩存(本地+Redis)
八、實現Caffeine與Redis多級緩存完整策略(待完善)?
在現代高并發系統中,多級緩存架構已成為提升系統性能的關鍵手段。Spring Cache通過抽象緩存接口,結合Caffeine(一級緩存)和Redis(二級緩存),可以構建高效的多級緩存解決方案。