SpringBoot集成本地緩存性能之王Caffeine示例詳解
引言
使用緩存的目的就是提高性能,今天碼哥帶大家實踐運(yùn)用 spring-boot-starter-cache 抽象的緩存組件去集成本地緩存性能之王 Caffeine。
大家需要注意的是:in-memeory 緩存只適合在單體應(yīng)用,不適合與分布式環(huán)境。
分布式環(huán)境的情況下需要將緩存修改同步到每個節(jié)點,需要一個同步機(jī)制保證每個節(jié)點緩存數(shù)據(jù)最終一致。
Spring Cache 是什么
不使用 Spring Cache 抽象的緩存接口,我們需要根據(jù)不同的緩存框架去實現(xiàn)緩存,需要在對應(yīng)的代碼里面去對應(yīng)緩存加載、刪除、更新等。
比如查詢我們使用旁路緩存策略:先從緩存中查詢數(shù)據(jù),如果查不到則從數(shù)據(jù)庫查詢并寫到緩存中。
偽代碼如下:
public User getUser(long userId) {
// 從緩存查詢
User user = cache.get(userId);
if (user != null) {
return user;
}
// 從數(shù)據(jù)庫加載
User dbUser = loadDataFromDB(userId);
if (dbUser != null) {
// 設(shè)置到緩存中
cache.put(userId, dbUser)
}
return dbUser;
}我們需要寫大量的這種繁瑣代碼,Spring Cache 則對緩存進(jìn)行了抽象,提供了如下幾個注解實現(xiàn)了緩存管理:
- @Cacheable:觸發(fā)緩存讀取操作,用于查詢方法上,如果緩存中找到則直接取出緩存并返回,否則執(zhí)行目標(biāo)方法并將結(jié)果緩存。
- @CachePut:觸發(fā)緩存更新的方法上,與 Cacheable 相比,該注解的方法始終都會被執(zhí)行,并且使用方法返回的結(jié)果去更新緩存,適用于 insert 和 update 行為的方法上。
- @CacheEvict:觸發(fā)緩存失效,刪除緩存項或者清空緩存,適用于 delete 方法上。
除此之外,抽象的 CacheManager 既能集成基于本地內(nèi)存的單體應(yīng)用,也能集成 EhCache、Redis 等緩存服務(wù)器。
最方便的是通過一些簡單配置和注解就能接入不同的緩存框架,無需修改任何代碼。
集成 Caffeine
碼哥帶大家使用注解方式完成緩存操作的方式來集成,完整的代碼請訪問 github:在 pom.xml 文件添加如下依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>使用 JavaConfig 方式配置 CacheManager:
@Slf4j
@EnableCaching
@Configuration
public class CacheConfig {
@Autowired
@Qualifier("cacheExecutor")
private Executor cacheExecutor;
@Bean
public Caffeine<Object, Object> caffeineCache() {
return Caffeine.newBuilder()
// 設(shè)置最后一次寫入或訪問后經(jīng)過固定時間過期
.expireAfterAccess(7, TimeUnit.DAYS)
// 初始的緩存空間大小
.initialCapacity(500)
// 使用自定義線程池
.executor(cacheExecutor)
.removalListener(((key, value, cause) -> log.info("key:{} removed, removalCause:{}.", key, cause.name())))
// 緩存的最大條數(shù)
.maximumSize(1000);
}
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
caffeineCacheManager.setCaffeine(caffeineCache());
// 不緩存空值
caffeineCacheManager.setAllowNullValues(false);
return caffeineCacheManager;
}
}準(zhǔn)備工作搞定,接下來就是如何使用了。
@Slf4j
@Service
public class AddressService {
public static final String CACHE_NAME = "caffeine:address";
private static final AtomicLong ID_CREATOR = new AtomicLong(0);
private Map<Long, AddressDTO> addressMap;
public AddressService() {
addressMap = new ConcurrentHashMap<>();
addressMap.put(ID_CREATOR.incrementAndGet(), AddressDTO.builder().customerId(ID_CREATOR.get()).address("地址1").build());
addressMap.put(ID_CREATOR.incrementAndGet(), AddressDTO.builder().customerId(ID_CREATOR.get()).address("地址2").build());
addressMap.put(ID_CREATOR.incrementAndGet(), AddressDTO.builder().customerId(ID_CREATOR.get()).address("地址3").build());
}
@Cacheable(cacheNames = {CACHE_NAME}, key = "#customerId")
public AddressDTO getAddress(long customerId) {
log.info("customerId:{} 沒有走緩存,開始從數(shù)據(jù)庫查詢", customerId);
return addressMap.get(customerId);
}
@CachePut(cacheNames = {CACHE_NAME}, key = "#result.customerId")
public AddressDTO create(String address) {
long customerId = ID_CREATOR.incrementAndGet();
AddressDTO addressDTO = AddressDTO.builder().customerId(customerId).address(address).build();
addressMap.put(customerId, addressDTO);
return addressDTO;
}
@CachePut(cacheNames = {CACHE_NAME}, key = "#result.customerId")
public AddressDTO update(Long customerId, String address) {
AddressDTO addressDTO = addressMap.get(customerId);
if (addressDTO == null) {
throw new RuntimeException("沒有 customerId = " + customerId + "的地址");
}
addressDTO.setAddress(address);
return addressDTO;
}
@CacheEvict(cacheNames = {CACHE_NAME}, key = "#customerId")
public boolean delete(long customerId) {
log.info("緩存 {} 被刪除", customerId);
return true;
}
}使用 CacheName 隔離不同業(yè)務(wù)場景的緩存,每個 Cache 內(nèi)部持有一個 map 結(jié)構(gòu)存儲數(shù)據(jù),key 可用使用 Spring 的 Spel 表達(dá)式。
單元測試走起:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = CaffeineApplication.class)
@Slf4j
public class CaffeineApplicationTests {
@Autowired
private AddressService addressService;
@Autowired
private CacheManager cacheManager;
@Test
public void testCache() {
// 插入緩存 和數(shù)據(jù)庫
AddressDTO newInsert = addressService.create("南山大道");
// 要走緩存
AddressDTO address = addressService.getAddress(newInsert.getCustomerId());
long customerId = 2;
// 第一次未命中緩存,打印 customerId:{} 沒有走緩存,開始從數(shù)據(jù)庫查詢
AddressDTO address2 = addressService.getAddress(customerId);
// 命中緩存
AddressDTO cacheAddress2 = addressService.getAddress(customerId);
// 更新數(shù)據(jù)庫和緩存
addressService.update(customerId, "地址 2 被修改");
// 更新后查詢,依然命中緩存
AddressDTO hitCache2 = addressService.getAddress(customerId);
Assert.assertEquals(hitCache2.getAddress(), "地址 2 被修改");
// 刪除緩存
addressService.delete(customerId);
// 未命中緩存, 從數(shù)據(jù)庫讀取
AddressDTO hit = addressService.getAddress(customerId);
System.out.println(hit.getCustomerId());
}
}大家發(fā)現(xiàn)沒,只需要在對應(yīng)的方法上加上注解,就能愉快的使用緩存了。需要注意的是, 設(shè)置的 cacheNames 一定要對應(yīng),每個業(yè)務(wù)場景使用對應(yīng)的 cacheNames。
另外 key 可以使用 spel 表達(dá)式,大家重點可以關(guān)注 @CachePut(cacheNames = {CACHE_NAME}, key = "#result.customerId"),result 表示接口返回結(jié)果,Spring 提供了幾個元數(shù)據(jù)直接使用。
| 名稱 | 地點 | 描述 | 例子 |
|---|---|---|---|
| methodName | 根對象 | 被調(diào)用的方法的名稱 | #root.methodName |
| method | 根對象 | 被調(diào)用的方法 | #root.method.name |
| target | 根對象 | 被調(diào)用的目標(biāo)對象 | #root.target |
| targetClass | 根對象 | 被調(diào)用的目標(biāo)的類 | #root.targetClass |
| args | 根對象 | 用于調(diào)用目標(biāo)的參數(shù)(作為數(shù)組) | #root.args[0] |
| caches | 根對象 | 運(yùn)行當(dāng)前方法的緩存集合 | #root.caches[0].name |
| 參數(shù)名稱 | 評估上下文 | 任何方法參數(shù)的名稱。如果名稱不可用(可能是由于沒有調(diào)試信息),則參數(shù)名稱也可在#a<#arg> where#arg代表參數(shù)索引(從 開始0)下獲得。 | #iban或#a0(您也可以使用#p0或#p<#arg>表示法作為別名)。 |
| result | 評估上下文 | 方法調(diào)用的結(jié)果(要緩存的值)。僅在unless 表達(dá)式、cache put表達(dá)式(計算key)或cache evict 表達(dá)式(when beforeInvocationis false)中可用。對于支持的包裝器(例如 Optional),#result指的是實際對象,而不是包裝器。 | #result |
核心原理
Java Caching定義了5個核心接口,分別是 CachingProvider, CacheManager, Cache, Entry 和 Expiry。

核心類圖:

- Cache:抽象了緩存的操作,比如,get()、put();
- CacheManager:管理 Cache,可以理解成 Cache 的集合管理,之所以有多個 Cache,是因為可以根據(jù)不同場景使用不同的緩存失效時間和數(shù)量限制。
- CacheInterceptor、CacheAspectSupport、AbstractCacheInvoker:CacheInterceptor 是一個AOP 方法攔截器,在方法前后做額外的邏輯,比如查詢操作,先查緩存,找不到數(shù)據(jù)再執(zhí)行方法,并把方法的結(jié)果寫入緩存等,它繼承了CacheAspectSupport(緩存操作的主體邏輯)、AbstractCacheInvoker(封裝了對 Cache 的讀寫)。
- CacheOperation、AnnotationCacheOperationSource、SpringCacheAnnotationParser:CacheOperation定義了緩存操作的緩存名字、緩存key、緩存條件condition、CacheManager等,AnnotationCacheOperationSource 是一個獲取緩存注解對應(yīng) CacheOperation 的類,而SpringCacheAnnotationParser 是解析注解的類,解析后會封裝成 CacheOperation 集合供AnnotationCacheOperationSource 查找。
CacheAspectSupport:緩存切面支持類,是CacheInterceptor 的父類,封裝了所有的緩存操作的主體邏輯。
主要流程如下:
- 通過CacheOperationSource,獲取所有的CacheOperation列表
- 如果有@CacheEvict注解、并且標(biāo)記為在調(diào)用前執(zhí)行,則做刪除/清空緩存的操作
- 如果有@Cacheable注解,查詢緩存
- 如果緩存未命中(查詢結(jié)果為null),則新增到cachePutRequests,后續(xù)執(zhí)行原始方法后會寫入緩存
- 緩存命中時,使用緩存值作為結(jié)果;緩存未命中、或有@CachePut注解時,需要調(diào)用原始方法,使用原始方法的返回值作為結(jié)果
- 如果有@CachePut注解,則新增到cachePutRequests
- 如果緩存未命中,則把查詢結(jié)果值寫入緩存;如果有@CachePut注解,也把方法執(zhí)行結(jié)果寫入緩存
- 如果有@CacheEvict注解、并且標(biāo)記為在調(diào)用后執(zhí)行,則做刪除/清空緩存的操作
今天就到這了,分享一些工作小技巧給大家,后面碼哥會分享如何接入 Redis ,并且?guī)Т蠹覍崿F(xiàn)一個基于 Sping Boot 實現(xiàn)一個 Caffeine 作為一級緩存、Redis 作為二級緩存的分布式二級緩存框架。
我們下期見,大家可以在評論區(qū)叫我靚仔么?不叫也行,點贊分享也是鼓勵。
參考資料
[1]http://www.dhdzp.com/article/242800.htm
[2]https://docs.spring.io/spring...
以上就是SpringBoot集成本地緩存性能之王Caffeine示例詳解的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot集成Caffeine的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
java switch語句使用注意的四大細(xì)節(jié)
很多朋友在使用java switch語句時,可能沒有注意到一些細(xì)節(jié),本文將詳細(xì)介紹使用java switch語句四大要點,需要的朋友可以參考下2012-12-12
springboot jpa分庫分表項目實現(xiàn)過程詳解
這篇文章主要介紹了springboot jpa分庫分表項目實現(xiàn)過程詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-01-01
SpringSecurity 默認(rèn)表單登錄頁展示流程源碼
本篇主要講解 SpringSecurity提供的默認(rèn)表單登錄頁 它是如何展示流程,本文圖文并茂給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友參考下吧2020-01-01
springboot+springsecurity如何實現(xiàn)動態(tài)url細(xì)粒度權(quán)限認(rèn)證
這篇文章主要介紹了springboot+springsecurity如何實現(xiàn)動態(tài)url細(xì)粒度權(quán)限認(rèn)證的操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-06-06
RabbitMq中channel接口的幾種常用參數(shù)詳解
這篇文章主要介紹了RabbitMq中channel接口的幾種常用參數(shù)詳解,RabbitMQ 不會為未確認(rèn)的消息設(shè)置過期時間,它判斷此消息是否需要重新投遞給消費(fèi)者的唯一依據(jù)是消費(fèi)該消息的消費(fèi)者連接是否己經(jīng)斷開,需要的朋友可以參考下2023-08-08

