SpringBoot 如何快速使用 Caffeine 緩存?
引言
前面我們有學(xué)習(xí)Caffeine 《本地緩存性能之王Caffeine》,并且也提到SpringBoot默認(rèn)使用的本地緩存也是Caffeine啦,今天我們來看看Caffeine如何與SpringBoot集成的。
集成caffeine
caffeine與SpringBoot集成有兩種方式:
- 一種是我們直接引入 Caffeine 依賴,然后使用 Caffeine 方法實現(xiàn)緩存。相當(dāng)于使用原生api
- 引入 Caffeine 和 Spring Cache 依賴,使用 SpringCache 注解方法實現(xiàn)緩存。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,通過構(gòu)造者模式構(gòu)建一個Cache對象,然后后續(xù)關(guān)于緩存的增刪查都是基于這個cache對象。
- @Configuration
- public class CacheConfig {
- @Bean
- public Cache<String, Object> caffeineCache() {
- return Caffeine.newBuilder()
- // 設(shè)置最后一次寫入或訪問后經(jīng)過固定時間過期
- .expireAfterWrite(60, TimeUnit.SECONDS)
- // 初始的緩存空間大小
- .initialCapacity(100)
- // 緩存的最大條數(shù)
- .maximumSize(1000)
- .build();
- }
第一種方式我們就一一不介紹了,基本上就是使用caffeineCache來根據(jù)你自己的業(yè)務(wù)來操作以下方法
這種方式使用的話是對代碼有侵入性的。
第二種方式
- 需要在SpingBoot啟動類標(biāo)上EnableCaching注解,這個玩意跟很多框架都一樣,比如我們肴集成dubbo也需要標(biāo)上@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
如果我們不習(xí)慣使用這種方式的配置,當(dāng)然我們也可以使用JavaConfig的配置方式來代替配置文件。
- @Configuration
- public class CacheConfig {
- @Bean
- public CacheManager cacheManager() {
- CaffeineCacheManager cacheManager = new CaffeineCacheManager();
- cacheManager.setCaffeine(Caffeine.newBuilder()
- // 設(shè)置最后一次寫入或訪問后經(jīng)過固定時間過期
- .expireAfterAccess(600, TimeUnit.SECONDS)
- // 初始的緩存空間大小
- .initialCapacity(100)
- // 緩存的最大條數(shù)
- .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 + "的數(shù)據(jù)緩存");
- }
- @Override
- @Cacheable(value = "user",key = "#id")
- public UserDTO getUserById(Long id) {
- return userRepository.findOne(id);
- }
上述代碼中我們可以看到有幾個注解@CachePut、@CacheEvict、@Cacheable我們只需要在方法上標(biāo)上這幾個注解,我們就能夠使用緩存了,我們分別來介紹下這幾個注解。
@Cacheable
@Cacheable它是既可以標(biāo)注在類上也可以標(biāo)注在方法上,當(dāng)它標(biāo)記在類上的時候它表述這個類上面的所有方法都會支持緩存,同樣的 當(dāng)它作用在法上面時候它表示這個方法是支持緩存的。比如上面我們代碼中的getUserById這個方法第一次緩存里面沒有數(shù)據(jù),我們會去查詢DB,但是第二次來查詢的時候就不會走DB查詢了,而是直接從緩存里面拿到結(jié)果就返回了。
value 屬性
- @Cacheable的value屬性是必須指定的,其表示當(dāng)前方法的返回值是會被緩存在哪個Cache上的,對應(yīng)Cache的名稱。
key
- @Cacheable的key 有兩種方式一種是我們自己顯示的去指定我們的key,還有一種默認(rèn)的生成策略,默認(rèn)的生成策略是SimpleKeyGenerator這個類,這個生成key的方式也比較簡單我們可以看下它的源碼:
- public static Object generateKey(Object... params) {
- // 如果方法沒有參數(shù) key就是一個 new SimpleKey()
- if (params.length == 0) {
- return SimpleKey.EMPTY;
- }
- // 如果方法只有一個參數(shù) key就是當(dāng)前參數(shù)
- if (params.length == 1) {
- Object param = params[0];
- if (param != null && !param.getClass().isArray()) {
- return param;
- }
- }
- // 如果key是多個參數(shù),key就是new SimpleKey ,不過這個SimpleKey對象的hashCode 和Equals方法是根據(jù)方法傳入的參數(shù)重寫的。
- return new SimpleKey(params);
- }
上述代碼還是非常好理解的分為三種情況:
- 方法沒有參數(shù),那就new使用一個全局空的SimpleKey對象來作為key。
- 方法就一個參數(shù),就使用當(dāng)前參數(shù)來作為key
- 方法參數(shù)大于1個,就new一個SimpleKey對象來作為key,new 這個SimpleKey的時候用傳入的參數(shù)重寫了SimpleKey的hashCode和equals方法, 至于為啥需要重寫的原因話,就跟Map用自定義對象來作為key的時候必須要重寫hashCode和equals方法原理是一樣的,因為caffein也是借助了ConcurrentHashMap來實現(xiàn),
小結(jié)
上述代碼我們可以發(fā)現(xiàn)默認(rèn)生成key只跟我們傳入的參數(shù)有關(guān)系,如果我們有一個類里面如果存在多個沒有參數(shù)的方法,然后我們使用了默認(rèn)的緩存生成策略的話,就會造成緩存丟失?;蛘呔彺嫦嗷ジ采w,或者還有可能會發(fā)生ClassCastException 因為都是使用同一個key。比如下面這代碼就會發(fā)生異常(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;
- }
所以一般不怎么推薦使用默認(rèn)的緩存生成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表達(dá)式來指定我們的key。這里的EL表達(dá)式可以使用方法參數(shù)及它們對應(yīng)的屬性。使用方法參數(shù)時我們可以直接使用“#參數(shù)名”或者“#p參數(shù)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一樣的,但是它們兩個是有區(qū)別的,@CachePut標(biāo)注的方法不會先去查詢緩存是否有值,而是每次都會先去執(zhí)行該方法,然后把結(jié)果返回,并且結(jié)果也會緩存起來。
為什么是這樣的一個流程我們可以去看看它的源碼關(guān)鍵代碼就是這一行,
- Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
當(dāng)我們使用方法上有@Cacheable注解的時候再contexts里面會把CacheableOperation加入進(jìn)去,只有contexts.get(CacheableOperation.class)取到的內(nèi)容不為空的話,才會去從緩存里面取內(nèi)容,否則的話cacheHit會直接返回null。至于contexts什么時候加入CacheableOperation的話我們看下SpringCacheAnnotationParser#parseCacheAnnotations這個方法就會明白的。具體的源碼就不展示了,感興趣的可以自己去翻。
@CacheEvict
把緩存中數(shù)據(jù)刪除,用法跟前面兩個注解差不多有value和key屬性,需要注意一點的是它多了一個屬性beforeInvocation
- beforeInvocation 這個屬性需要注意下它的默認(rèn)值是false,false代表的意思是再執(zhí)調(diào)用方法之前不刪除緩存,只有方法執(zhí)行成功之后才會去刪除緩存。設(shè)置為true的話調(diào)用方法之前會去刪除一下緩存,方法執(zhí)行成功之后還會去調(diào)用刪除緩存這樣就是雙刪了。如果方法執(zhí)行異常的話就不會去刪除緩存。
- allEntrie 是否清空所有緩存內(nèi)容,默認(rèn)值為 false,如果指定為 true,則方法調(diào)用后將立即清空所有緩存
@Caching
這是一個組合注解集成了上面三個注解,有三個屬性:cacheable、put和evict,分別用于來指定@Cacheable、@CachePut和@CacheEvict。
小結(jié)
第二種方式是侵入式的,它的實現(xiàn)原理也比較簡單就是通過切面的方法攔截器來實現(xiàn),攔截所有的方法,它的核心代碼如下:看起來就跟我們的業(yè)務(wù)代碼差不了多少,感興趣的也可以去瞅一瞅。
- 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;
- }
結(jié)束
由于自己才疏學(xué)淺,難免會有紕漏,假如你發(fā)現(xiàn)了錯誤的地方,還望留言給我指出來,我會對其加以修正。
感謝您的閱讀,十分歡迎并感謝您的關(guān)注。
站在巨人的肩膀上摘蘋果: https://www.cnblogs.com/fashflying/p/6908028.html#!comments