SpringBoot如何整合Caffeine?

java金融發表於2021-11-05

引言

前面我們有學習Caffeine 本地快取效能之王Caffeine,並且也提到SpringBoot預設使用的本地快取也是Caffeine啦,今天我們來看看Caffeine如何與SpringBoot整合的。

整合caffeine

caffeineSpringBoot整合有兩種方式:

  • 一種是我們直接引入 Caffeine 依賴,然後使用 Caffeine 方法實現快取。相當於使用原生api
  • 引入 CaffeineSpring 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 屬性
  • @Cacheablevalue屬性是必須指定的,其表示當前方法的返回值是會被快取在哪個Cache上的,對應Cache的名稱。

    key
  • @Cacheablekey 有兩種方式一種是我們自己顯示的去指定我們的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物件來作為keynew 這個SimpleKey的時候用傳入的引數重寫了SimpleKeyhashCodeequals方法,
    至於為啥需要重寫的原因話,就跟Map用自定義物件來作為key的時候必須要重寫hashCodeequals方法原理是一樣的,因為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

相關文章