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


問(wèn)題:明明調(diào)用接口寫入了緩存,為什么我們查詢的時(shí)候還是沒(méi)有呢?
細(xì)心的你可能已經(jīng)發(fā)現(xiàn)了,我們?cè)诿總€(gè)接口都重新構(gòu)造了一個(gè)新的Cache實(shí)例。這兩個(gè)Cache實(shí)例是完全獨(dú)立的,數(shù)據(jù)不會(huì)自動(dòng)共享。
解決辦法
所以,聰明的你可能就想著把它提取出來(lái),成功公共變量吧
@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;
}
}
你看這不就有了!于是聰明的你,又想:“如果放在這個(gè)控制器類下面,那我其他類中要是想調(diào)用,是不是不太好?”
于是你又把它放在一個(gè)配置類下面,用于專門管理緩存。
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ā)現(xiàn)依然可以呀!真棒!
于是你又靈機(jī)一動(dòng),多定義幾個(gè)bean吧,一個(gè)設(shè)置有效期,一個(gè)永不過(guò)期。
@Configuration
public class CacheConfig {
@Bean("noLimit")
public Cache<String, Object> buildCache() {
return Caffeine.newBuilder().build();
}
@Bean("limited")
public Cache<String, Object> buildLimitedCache() {
// 設(shè)置過(guò)期時(shí)間是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ā)現(xiàn)30s后加入的緩存也沒(méi)有了。
3.3 小結(jié)
通過(guò)這個(gè)案例,你似乎也覺(jué)察到了,Caffeine的基本使用方法
- 導(dǎo)入依賴
- 構(gòu)建公共緩存對(duì)象(expireAfterWrite方法可以設(shè)置寫入后多久過(guò)期)
- 使用 put() 方法添加緩存
- 使用 getIfPresent() 方法讀取緩存
- 一旦重啟項(xiàng)目,緩存就都消失了(基于本地內(nèi)存)!
四、Caffeine常用方法詳解
4.1 getIfPresent
@Nullable V getIfPresent(@CompatibleWith("K") @NonNull Object var1);
前面已經(jīng)演示過(guò)了,這里就不在舉例了。意思是如果存在則獲取,不存在就是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 -> {
// 緩存不存在時(shí),執(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);
入門案例也演示過(guò)了,就是添加緩存。使用方法和普通的map類似,都是key,value的形式。
4.4 putAll
void putAll(@NonNull Map<? extends @NonNull K, ? extends @NonNull V> var1);
putAll 顧名思義,就是可以批量寫入緩存。首先定義一個(gè)map對(duì)象,把要加入的緩存往map里面塞,然后把map作為參數(shù)傳遞給這個(gè)方法即可。
4.5 invalidate
手動(dòng)清除單個(gè)緩存
cache.invalidate("key1");4.6 invalidateAll
手動(dòng)批量清除多個(gè)key
// 批量清除多個(gè)key
cache.invalidateAll(Arrays.asList("key1", "key2"));手動(dòng)清除所有緩存
// 清除所有緩存 cache.invalidateAll();
??注意:
這些方法會(huì)立即從緩存中移除指定的條目。
Caffeine除了手動(dòng)清除外,也和Redis一樣,有自動(dòng)清除策略。這些將在下一張集中講解。
五、構(gòu)建一個(gè)更加全面的緩存
前面我們演示時(shí),通過(guò)Caffeine.newBuilder().build();就建完了緩存對(duì)象,頂多給它設(shè)置了一個(gè)過(guò)期時(shí)間。
但是關(guān)于這個(gè)緩存對(duì)象本身,還有很多東西是可以設(shè)置的,下面我們就詳細(xì)說(shuō)說(shuō),還有哪些設(shè)置。
Caffeine.newBuilder() 提供了豐富的配置選項(xiàng),可以創(chuàng)建高性能、靈活的緩存實(shí)例。以下是主要的可配置內(nèi)容:
5.1、容量控制配置
(1)??initialCapacity(int)??
設(shè)置初始緩存容量
示例:.initialCapacity(100)表示初始能存儲(chǔ)100個(gè)緩存對(duì)象
(2)??maximumSize(long)??
按條目數(shù)量限制緩存大小
示例:.maximumSize(1000)表示最多緩存1000個(gè)條目
(3)??maximumWeight(long)??
按自定義權(quán)重總和限制緩存大小
需要配合weigher()使用
示例:.maximumWeight(10000).weigher((k,v) -> ((User)v).getSize())
注意:maximumSize和maximumWeight不能同時(shí)使用
當(dāng)緩存條目數(shù)超過(guò)最大設(shè)定值時(shí),Caffeine會(huì)根據(jù)Window TinyLFU算法自動(dòng)清除"最不常用"的條目
5.2、過(guò)期策略配置
(1)expireAfterAccess(long, TimeUnit)??
設(shè)置最后訪問(wèn)后過(guò)期時(shí)間
示例:.expireAfterAccess(5, TimeUnit.MINUTES)
(2)??expireAfterWrite(long, TimeUnit)?
設(shè)置創(chuàng)建/更新后過(guò)期時(shí)間
示例:.expireAfterWrite(10, TimeUnit.MINUTES)
?(3)?expireAfter(Expiry)??
自定義過(guò)期策略
可以基于創(chuàng)建、更新、讀取事件分別設(shè)置
.expireAfter(new Expiry<String, Object>() {
public long expireAfterCreate(String key, Object value, long currentTime) {
return TimeUnit.HOURS.toNanos(1); // 創(chuàng)建1小時(shí)后過(guò)期
}
public long expireAfterUpdate(String key, Object value, long currentTime, long currentDuration) {
return currentDuration; // 保持原過(guò)期時(shí)間
}
public long expireAfterRead(String key, Object value, long currentTime, long currentDuration) {
return currentDuration; // 保持原過(guò)期時(shí)間
}
})5.3 注意事項(xiàng)
Caffeine的清除操作通常是異步執(zhí)行的,如果需要立即清理所有過(guò)期條目,可以調(diào)用:
cache.cleanUp();
這個(gè)方法會(huì)觸發(fā)一次完整的緩存清理,包括所有符合條件的過(guò)期條目。
六、整合Spring Cache
前面介紹時(shí)說(shuō)了,Caffeine作為Spring體系中內(nèi)置的緩存之一,Spring Cache同樣提供調(diào)用接口支持。所以接下來(lái),我們?cè)敿?xì)實(shí)現(xiàn)整合過(guò)程。
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) // 最大緩存條目數(shù)
.expireAfterWrite(10, TimeUnit.MINUTES) // 寫入后10分鐘過(guò)期
.expireAfterAccess(5, TimeUnit.MINUTES) // 訪問(wèn)后5分鐘過(guò)期
.weakKeys() // 使用弱引用鍵
.recordStats()); // 記錄統(tǒng)計(jì)信息
return cacheManager;
}
}6.3 使用
具體使用方法可以參考前面寫的這篇文章Spring Cache用法很簡(jiǎn)單,但你知道這中間的坑嗎?
springcache無(wú)非就是那幾個(gè)注解。這里淺淺舉例演示
@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控制臺(tái)也僅僅打印了一次---------------------------
說(shuō)明緩存生效了!
七、生產(chǎn)環(huán)境注意事項(xiàng)
提到緩存,那就是老生常談的:緩存穿透、緩存擊穿和緩存雪崩等問(wèn)題。
緩存穿透防護(hù)??:
- 對(duì)null值進(jìn)行適當(dāng)緩存(使用
unless = "#result == null") - 考慮使用Bloom過(guò)濾器
??緩存雪崩防護(hù)??:
- 為不同緩存設(shè)置不同的過(guò)期時(shí)間
- 添加隨機(jī)抖動(dòng)因子到過(guò)期時(shí)間
??緩存一致性??:
- 重要數(shù)據(jù)建議配合數(shù)據(jù)庫(kù)事務(wù)
- 考慮使用
@CachePut更新策略
??內(nèi)存管理??:
- 合理設(shè)置
maximumSize防止OOM - 對(duì)大對(duì)象考慮使用
weakValues()或softValues()
??分布式環(huán)境??:
- 本地緩存需要配合消息總線實(shí)現(xiàn)多節(jié)點(diǎn)同步
- 或考慮使用多級(jí)緩存(本地+Redis)
八、實(shí)現(xiàn)Caffeine與Redis多級(jí)緩存完整策略(待完善)
在現(xiàn)代高并發(fā)系統(tǒng)中,多級(jí)緩存架構(gòu)已成為提升系統(tǒng)性能的關(guān)鍵手段。Spring Cache通過(guò)抽象緩存接口,結(jié)合Caffeine(一級(jí)緩存)和Redis(二級(jí)緩存),可以構(gòu)建高效的多級(jí)緩存解決方案。
到此這篇關(guān)于Caffeine本地緩存核心原理與使用方法詳解(含與Spring Cache集成)的文章就介紹到這了,更多相關(guān)Caffeine本地緩存內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
簡(jiǎn)單快速對(duì)@RequestParam聲明的參數(shù)作校驗(yàn)操作
這篇文章主要介紹了簡(jiǎn)單快速對(duì)@RequestParam聲明的參數(shù)作校驗(yàn)操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08
Mybatis update數(shù)據(jù)庫(kù)死鎖之獲取數(shù)據(jù)庫(kù)連接池等待
這篇文章主要介紹了Mybatis update數(shù)據(jù)庫(kù)死鎖之獲取數(shù)據(jù)庫(kù)連接池等待的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-07-07
詳解spring security之httpSecurity使用示例
這篇文章主要介紹了詳解spring security之httpSecurity使用示例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-08-08
SpringBoot實(shí)現(xiàn)api加密的示例代碼
在項(xiàng)目中,為了保證數(shù)據(jù)的安全,我們常常會(huì)對(duì)傳遞的數(shù)據(jù)進(jìn)行加密。本文主要介紹了SpringBoot實(shí)現(xiàn)api加密的示例代碼,感興趣的小伙伴們可以參考一下2021-07-07
Spring中事務(wù)用法示例及實(shí)現(xiàn)原理詳解
這篇文章主要給大家介紹了關(guān)于Spring中事務(wù)用法示例及實(shí)現(xiàn)原理的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-11-11
Java簡(jiǎn)單模擬實(shí)現(xiàn)一個(gè)線程池
本文主要介紹了Java簡(jiǎn)單模擬實(shí)現(xiàn)一個(gè)線程池,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-01-01

