引言
前面我們有學習Caffeine
本地快取效能之王Caffeine,並且也提到SpringBoot
預設使用的本地快取也是Caffeine
啦,今天我們來看看Caffeine
如何與SpringBoot
整合的。
整合caffeine
caffeine
與SpringBoot
整合有兩種方式:
- 一種是我們直接引入
Caffeine
依賴,然後使用Caffeine
方法實現快取。相當於使用原生api 引入
Caffeine
和Spring Cache
依賴,使用SpringCache
註解方法實現快取。SpringCache幫我們封裝了Caffeine
pom檔案引入<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> <version>2.6.0</version> </dependency>
第一種方式
首先配置一個
Cache
,通過構造者模式構建一個Cache
物件,然後後續關於快取的增刪查都是基於這個cache
物件。@Configuration public class CacheConfig { @Bean public Cache<String, Object> caffeineCache() { return Caffeine.newBuilder() // 設定最後一次寫入或訪問後經過固定時間過期 .expireAfterWrite(60, TimeUnit.SECONDS) // 初始的快取空間大小 .initialCapacity(100) // 快取的最大條數 .maximumSize(1000) .build(); }
第一種方式我們就一一不介紹了,基本上就是使用
caffeineCache
來根據你自己的業務來操作以下方法
這種方式使用的話是對程式碼有侵入性的。第二種方式
需要在SpingBoot啟動類標上
EnableCaching
註解,這個玩意跟很多框架都一樣,比如我們餚整合dubbo
也需要標上@EnableDubbole
註解等。@SpringBootApplication @EnableCaching public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); }
在
application.yml
配置我們的使用的快取型別、過期時間、快取策略等。spring: profiles: active: dev cache: type: CAFFEINE caffeine: spec: maximumSize=500,expireAfterAccess=600s
如果我們不習慣使用這種方式的配置,當然我們也可以使用
JavaConfig
的配置方式來代替配置檔案。@Configuration public class CacheConfig { @Bean public CacheManager cacheManager() { CaffeineCacheManager cacheManager = new CaffeineCacheManager(); cacheManager.setCaffeine(Caffeine.newBuilder() // 設定最後一次寫入或訪問後經過固定時間過期 .expireAfterAccess(600, TimeUnit.SECONDS) // 初始的快取空間大小 .initialCapacity(100) // 快取的最大條數 .maximumSize(500)); return cacheManager; }
接下來就是程式碼中如何來使用這個快取了
@Override @CachePut(value = "user", key = "#userDTO.id") public UserDTO save(UserDTO userDTO) { userRepository.save(userDTO); return userDTO; } @Override @CacheEvict(value = "user", key = "#id")//2 public void remove(Long id) { logger.info("刪除了id、key為" + id + "的資料快取"); } @Override @Cacheable(value = "user",key = "#id") public UserDTO getUserById(Long id) { return userRepository.findOne(id); }
上述程式碼中我們可以看到有幾個註解
@CachePut、@CacheEvict、@Cacheable
我們只需要在方法上標上這幾個註解,我們就能夠使用快取了,我們分別來介紹下這幾個註解。@Cacheable
@Cacheable
它是既可以標註在類上也可以標註在方法上,當它標記在類上的時候它表述這個類上面的所有方法都會支援快取,同樣的
當它作用在法上面時候它表示這個方法是支援快取的。比如上面我們程式碼中的getUserById
這個方法第一次快取裡面沒有資料,我們會去查詢DB
,但是第二次來查詢的時候就不會走DB
查詢了,而是直接從快取裡面拿到結果就返回了。value 屬性
@Cacheable
的value
屬性是必須指定的,其表示當前方法的返回值是會被快取在哪個Cache
上的,對應Cache
的名稱。key
@Cacheable
的key
有兩種方式一種是我們自己顯示的去指定我們的key
,還有一種預設的生成策略,預設的生成策略是SimpleKeyGenerator
這個類,這個生成key
的方式也比較簡單我們可以看下它的原始碼:public static Object generateKey(Object... params) { // 如果方法沒有引數 key就是一個 new SimpleKey() if (params.length == 0) { return SimpleKey.EMPTY; } // 如果方法只有一個引數 key就是當前引數 if (params.length == 1) { Object param = params[0]; if (param != null && !param.getClass().isArray()) { return param; } } // 如果key是多個引數,key就是new SimpleKey ,不過這個SimpleKey物件的hashCode 和Equals方法是根據方法傳入的引數重寫的。 return new SimpleKey(params); }
上述程式碼還是非常好理解的分為三種情況:
- 方法沒有引數,那就new使用一個全域性空的
SimpleKey
物件來作為key
。 - 方法就一個引數,就使用當前引數來作為
key
方法引數大於
1
個,就new
一個SimpleKey
物件來作為key
,new
這個SimpleKey
的時候用傳入的引數重寫了SimpleKey
的hashCode
和equals
方法,
至於為啥需要重寫的原因話,就跟Map
用自定義物件來作為key
的時候必須要重寫hashCode
和equals
方法原理是一樣的,因為caffein
也是藉助了ConcurrentHashMap
來實現,小結
上述程式碼我們可以發現預設生成
key
只跟我們傳入的引數有關係,如果我們有一個類裡面如果存在多個沒有引數的方法,然後我們使用了預設的快取生成策略的話,就會造成快取丟失。
或者快取相互覆蓋,或者還有可能會發生ClassCastException
因為都是使用同一個key
。比如下面這程式碼就會發生異常(ClassCastException
)@Cacheable(value = "user") public UserDTO getUser() { UserDTO userDTO = new UserDTO(); userDTO.setUserName("Java金融"); return userDTO; } @Cacheable(value = "user") public UserDTO2 getUser1() { UserDTO2 userDTO2 = new UserDTO2(); userDTO2.setUserName2("javajr.cn"); return userDTO2; }
所以一般不怎麼推薦使用預設的快取生成
key
的策略。如果非要用的話我們最好自己重寫一下,帶上方法名字等。類似於如下程式碼:@Component public class MyKeyGenerator extends SimpleKeyGenerator { @Override public Object generate(Object target, Method method, Object... params) { Object generate = super.generate(target, method, params); String format = MessageFormat.format("{0}{1}{2}", method.toGenericString(), generate); return format; }
自定義key
我們可以通過
Spring
的EL表示式來指定我們的key
。這裡的EL表示式可以使用方法引數及它們對應的屬性。
使用方法引數時我們可以直接使用“#引數名
”或者“#p引數index
”這也是我們比較推薦的做法:@Cacheable(value="user", key="#id") public UserDTO getUserById(Long id) { UserDTO userDTO = new UserDTO(); userDTO.setUserName("java金融"); return userDTO; } @Cacheable(value="user", key="#p0") public UserDTO getUserById1(Long id) { return null; } @Cacheable(value="user", key="#userDTO.id") public UserDTO getUserById2(UserDTO userDTO) { return null; } @Cacheable(value="user", key="#p0.id") public UserDTO getUserById3(UserDTO userDTO) { return null; }
@CachePut
@CachePut
指定的屬性是和@Cacheable
一樣的,但是它們兩個是有區別的,@CachePut
標註的方法不會先去查詢快取是否有值,而是每次都會先去執行該方法,然後把結果返回,並且結果也會快取起來。![在這裡插入圖片描述](https://img-blog.csdnimg.cn/ff516023113046adbf86caaea6e499f6.png)
為什麼是這樣的一個流程我們可以去看看它的原始碼關鍵程式碼就是這一行,
Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
當我們使用方法上有@Cacheable
註解的時候再contexts
裡面會把CacheableOperation
加入進去,只有contexts.get(CacheableOperation.class)取到的內容不為空的話,才會去從快取裡面取內容,否則的話cacheHit
會直接返回null
。至於contexts什麼時候加入CacheableOperation的話我們看下SpringCacheAnnotationParser#parseCacheAnnotations
這個方法就會明白的。具體的原始碼就不展示了,感興趣的可以自己去翻。
@CacheEvict
把快取中資料刪除,用法跟前面兩個註解差不多有value和key屬性,需要注意一點的是它多了一個屬性beforeInvocation
beforeInvocation
這個屬性需要注意下它的預設值是false,false代表的意思是再執呼叫方法之前不刪除快取,只有方法執行成功之後才會去刪除快取。設定為true
的話呼叫方法之前會去刪除一下快取,方法執行成功之後還會去呼叫刪除快取這樣就是雙刪了。如果方法執行異常的話就不會去刪除快取。allEntrie
是否清空所有快取內容,預設值為false
,如果指定為true
,則方法呼叫後將立即清空所有快取
@Caching
這是一個組合註解整合了上面三個註解,有三個屬性:cacheable、put和evict
,分別用於來指定@Cacheable
、@CachePut
和@CacheEvict
。
小結
第二種方式是侵入式的,它的實現原理也比較簡單就是通過切面的方法攔截器來實現,攔截所有的方法,它的核心程式碼如下:看起來就跟我們的業務程式碼差不了多少,感興趣的也可以去瞅一瞅。
if (contexts.isSynchronized()) {
CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();
if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
Cache cache = context.getCaches().iterator().next();
try {
return wrapCacheValue(method, cache.get(key, () -> unwrapReturnValue(invokeOperation(invoker))));
}
catch (Cache.ValueRetrievalException ex) {
// The invoker wraps any Throwable in a ThrowableWrapper instance so we
// can just make sure that one bubbles up the stack.
throw (CacheOperationInvoker.ThrowableWrapper) ex.getCause();
}
}
else {
// No caching required, only call the underlying method
return invokeOperation(invoker);
}
}
// Process any early evictions
// beforeInvocation 屬性是否為true,如果是true就刪除快取
processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
CacheOperationExpressionEvaluator.NO_RESULT);
// Check if we have a cached item matching the conditions
Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
// Collect puts from any @Cacheable miss, if no cached item is found
List<CachePutRequest> cachePutRequests = new LinkedList<>();
if (cacheHit == null) {
collectPutRequests(contexts.get(CacheableOperation.class),
CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
}
Object cacheValue;
Object returnValue;
if (cacheHit != null && !hasCachePut(contexts)) {
// If there are no put requests, just use the cache hit
cacheValue = cacheHit.get();
returnValue = wrapCacheValue(method, cacheValue);
}
else {
// Invoke the method if we don't have a cache hit
returnValue = invokeOperation(invoker);
cacheValue = unwrapReturnValue(returnValue);
}
// Collect any explicit @CachePuts
collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
// Process any collected put requests, either from @CachePut or a @Cacheable miss
for (CachePutRequest cachePutRequest : cachePutRequests) {
cachePutRequest.apply(cacheValue);
}
// Process any late evictions
processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);
return returnValue;
}
結束
- 由於自己才疏學淺,難免會有紕漏,假如你發現了錯誤的地方,還望留言給我指出來,我會對其加以修正。
- 如果你覺得文章還不錯,你的轉發、分享、讚賞、點贊、留言就是對我最大的鼓勵。
- 感謝您的閱讀,十分歡迎並感謝您的關注。
站在巨人的肩膀上摘蘋果:
https://www.cnblogs.com/fashf...!comments