Caffeine本地緩存核心原理與使用方法詳解(含與Spring?Cache集成)
一、介紹
JDK內置的Map可作為緩存的一種實現方式,然而嚴格意義來講,其不能算作緩存的范疇。
原因如下:一是其存儲的數據不能主動過期;二是無任何緩存淘汰策略。
Caffeine是一個基于Java 8的高性能本地緩存庫,由Ben Manes開發(fā),旨在提供快速、靈活的緩存解決方案。作為Guava Cache的現代替代品,Caffeine在性能、功能和靈活性方面都有顯著提升。
Caffeine作為Spring體系中內置的緩存之一,Spring Cache同樣提供調用接口支持。已成為Java生態(tài)中最受歡迎的本地緩存庫之一。
本文將全面介紹Caffeine的核心原理、使用方法和最佳實踐。
二、Caffeine核心原理與架構設計
2.1 存儲引擎與數據結構
Caffeine底層采用優(yōu)化的ConcurrentHashMap作為主要存儲結構,并在此基礎上進行了多項創(chuàng)新:
- ??分段存儲技術??:使用StripedBuffer實現無鎖化并發(fā)控制,將競爭分散到多個獨立緩沖區(qū),顯著提升并發(fā)吞吐量。
- ??頻率統(tǒng)計機制??:采用Count-Min Sketch算法記錄訪問頻率,以93.75%的準確率僅使用少量內存空間。
- ??時間輪管理??:通過TimerWheel數據結構高效管理過期條目,實現納秒級精度的過期控制。
2.2 緩存淘汰策略
Caffeine采用了創(chuàng)新的Window-TinyLFU算法,結合了LRU和LFU的優(yōu)點:
- ??三區(qū)設計??:窗口區(qū)(20%)、試用區(qū)(1%)和主區(qū)(79%),各區(qū)使用LRU雙端隊列管理
- ??動態(tài)調整??:根據訪問模式自動調整各區(qū)比例,最高可實現98%的緩存命中率
- ??頻率衰減??:通過周期性衰減歷史頻率,防止舊熱點數據長期占據緩存
相比Guava Cache的LRU算法,Window-TinyLFU能更準確地識別和保留真正的熱點數據,避免"一次性訪問"污染緩存。
2.3 并發(fā)控制機制
Caffeine的并發(fā)控制體系設計精妙:
- ??寫緩沖機制??:使用RingBuffer和MpscChunkedArrayQueue實現多生產者-單消費者隊列
- ??樂觀鎖優(yōu)化??:通過ReadAndWriteCounterRef等自定義原子引用降低CAS開銷
- ??StampedLock應用??:在關鍵路徑上使用Java 8的StampedLock替代傳統(tǒng)鎖,提升并發(fā)性能
三、入門案例
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;
}
} 


問題:明明調用接口寫入了緩存,為什么我們查詢的時候還是沒有呢?
細心的你可能已經發(fā)現了,我們在每個接口都重新構造了一個新的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 {
@Bean
public Cache<String, Object> buildCache() {
return Caffeine.newBuilder().build();
}
}@RestController
@RequestMapping("/api")
public class Controller {
@Resource
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;
}
}
聰明的你,發(fā)現依然可以呀!真棒!
于是你又靈機一動,多定義幾個bean吧,一個設置有效期,一個永不過期。
@Configuration
public class CacheConfig {
@Bean("noLimit")
public Cache<String, Object> buildCache() {
return Caffeine.newBuilder().build();
}
@Bean("limited")
public Cache<String, Object> buildLimitedCache() {
// 設置過期時間是30s
return 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;
}
}

你發(fā)現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 -> {
// 緩存不存在時,執(zhí)行加載邏輯
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() 提供了豐富的配置選項,可以創(chuàng)建高性能、靈活的緩存實例。以下是主要的可配置內容:
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)?
設置創(chuàng)建/更新后過期時間
示例:.expireAfterWrite(10, TimeUnit.MINUTES)
?(3)?expireAfter(Expiry)??
自定義過期策略
可以基于創(chuàng)建、更新、讀取事件分別設置
.expireAfter(new Expiry<String, Object>() {
public long expireAfterCreate(String key, Object value, long currentTime) {
return TimeUnit.HOURS.toNanos(1); // 創(chuàng)建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的清除操作通常是異步執(zhí)行的,如果需要立即清理所有過期條目,可以調用:
cache.cleanUp();
這個方法會觸發(fā)一次完整的緩存清理,包括所有符合條件的過期條目。
六、整合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 {
@Bean
public 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()); // 記錄統(tǒng)計信息
return cacheManager;
}
}6.3 使用
具體使用方法可以參考前面寫的這篇文章Spring Cache用法很簡單,但你知道這中間的坑嗎?
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控制臺也僅僅打印了一次---------------------------
說明緩存生效了!
七、生產環(huán)境注意事項
提到緩存,那就是老生常談的:緩存穿透、緩存擊穿和緩存雪崩等問題。
緩存穿透防護??:
- 對null值進行適當緩存(使用
unless = "#result == null") - 考慮使用Bloom過濾器
??緩存雪崩防護??:
- 為不同緩存設置不同的過期時間
- 添加隨機抖動因子到過期時間
??緩存一致性??:
- 重要數據建議配合數據庫事務
- 考慮使用
@CachePut更新策略
??內存管理??:
- 合理設置
maximumSize防止OOM - 對大對象考慮使用
weakValues()或softValues()
??分布式環(huán)境??:
- 本地緩存需要配合消息總線實現多節(jié)點同步
- 或考慮使用多級緩存(本地+Redis)
八、實現Caffeine與Redis多級緩存完整策略(待完善)
在現代高并發(fā)系統(tǒng)中,多級緩存架構已成為提升系統(tǒng)性能的關鍵手段。Spring Cache通過抽象緩存接口,結合Caffeine(一級緩存)和Redis(二級緩存),可以構建高效的多級緩存解決方案。
到此這篇關于Caffeine本地緩存核心原理與使用方法詳解(含與Spring Cache集成)的文章就介紹到這了,更多相關Caffeine本地緩存內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java開發(fā)環(huán)境配置及Vscode搭建過程
今天通過圖文并茂的形式給大家介紹Java開發(fā)環(huán)境配置及Vscode搭建過程,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友參考下吧2021-07-07
關于springboot2整合lettuce啟動卡住問題的解決方法
Lettuce和Jedis的都是連接Redis Server的客戶端程序,下面這篇文章主要給大家介紹了關于springboot2整合lettuce啟動卡住問題的解決方法,文中通過示例代碼介紹的非常詳細,需要的朋友可以參考下2021-12-12
Java concurrency集合之ConcurrentSkipListMap_動力節(jié)點Java學院整理
這篇文章主要為大家詳細介紹了Java concurrency集合之ConcurrentSkipListMap的相關資料,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-06-06
Springboot?jpa使用sum()函數返回結果如何被接收
這篇文章主要介紹了Springboot?jpa使用sum()函數返回結果如何接收,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-02-02

