SpringBoot&Caffeine 靈活支持多個緩存配置策略


前言

緩存是幾乎所有應用程序性能的關鍵。很多時候需要分布式緩存(比如常用的 Redis、Codis),但在許多情況下,本地緩存也可以很好地工作,並且不需要分布式緩存的開銷和復雜性。

對於 DotNet 開發來說,本地 cache 很方便使用(比如 RuntimeCache 等); 對於 Java 說,也有很多優秀的本地 cache 庫(比如 Ehcache、GuavaCache 等),而 Java 這個帝國中,spring 是一個偉大且壟斷的存在。針對不同的緩存技術,Spring 定義了如下的 cacheManger 實現。

CacheManger 描述
SimpleCacheManager 使用簡單的 Collection 來存儲緩存,主要用於測試
ConcurrentMapCacheManager 使用 ConcurrentMap 作為緩存技術(默認),需要顯式的刪除緩存,無過期機制
NoOpCacheManager 僅測試用途,不會實際存儲緩存
EhCacheCacheManager 使用 EhCache 作為緩存技術,以前在 hibernate 的時候經常用
GuavaCacheManager 使用 google guava 的 GuavaCache 作為緩存技術(1.5 版本已不建議使用)
CaffeineCacheManager 是使用 Java8 對 Guava 緩存的重寫,spring5(springboot2)開始用 Caffeine 取代 guava
HazelcastCacheManager 使用 Hazelcast 作為緩存技術
JCacheCacheManager 使用 JCache 標准的實現作為緩存技術,如 Apache Commons JCS
RedisCacheManager 使用 Redis 作為緩存技術

因此,在許多應用程序中(包括普通的 Spring 和 Spring Boot),你都可以引入相應的依賴包后,簡單的把 @Cacheable 打在任何方法上使用它,以使其結果被緩存,以便下次調用該方法時,將返回緩存的結果。

Tips:spring cache 使用基於動態生成子類的代理機制來對方法的調用進行切面,如果緩存的方法是內部調用而不是外部引用,會導致代理失敗,切面失效。

雖然 Spring 有一些默認的緩存管理器實現,有時候,一些外部庫總是比簡單的實現更好、更靈活。例如,其中一個高性能的 Java 緩存庫 Caffeine 。

今天,我們主要目標就是:認識 Caffeine 以及如何實現多緩存靈活配置。

那么,簡單認識一下 Caffeine

Caffeine 是使用 Java8 對 Guava 緩存的重寫版本,緩存類似於 ConcurrentMap,但並不完全相同,可提供接近最佳的命中率。它基於 LRU 算法實現,支持多種緩存過期策略。在 Spring Boot 2.0 中將取代 GuavaCache。 特性如下:

  • 自動將實體加載到緩存中,可以選擇異步加載;
  • 基於頻率和新近度超過最大值時基於大小的淘汰;
  • 自上次訪問或上次寫入以來的基於時間的過期;
  • 基於第一個請求舊數據時的異步刷新(只放行一個請求去刷新數據);
  • 其他

Caffeine 的一些參數,我們后續也會用到

參數 描述
initialCapacity=[integer] 初始的緩存空間大小(比較常用)
maximumSize=[long] 緩存的最大條數 (比較常用)
maximumWeight=[long] 緩存的最大權重
expireAfterAccess=[duration] 最后一次寫入或訪問后經過固定時間過期 (比較常用)
expireAfterWrite=[duration] 最后一次寫入后經過固定時間過期(比較常用)
refreshAfterWrite=[duration] 創建緩存或者最近一次更新緩存后經過固定的時間間隔,刷新緩存 refreshAfterWrite requires a LoadingCache
weakKeys 打開 key 的弱引用
weakValues 打開 value 的弱引用
softValues 打開 value 的軟引用
recordStats 開發統計功能

注意:

  • refreshAfterWrite 必須實現 LoadingCache,跟 expire 的區別是,指定時間過后 expire 是 remove 該 key,下次訪問是同步去獲取返回新值,而 refresh 則是指定時間后,不會 remove 該 key,下次訪問會觸發刷新,新值沒有回來時返回舊值。
  • expireAfterWrite 和 expireAfterAccess 同時存在時,以 expireAfterWrite 為准。
  • maximumSize 和 maximumWeight 不可以同時使用。
  • weakValues 和 softValues 不可以同時使用。

簡單了解了 caffeine 是什么,有哪些屬性可用,那么我們回過頭來,你會發現,其實 SpringBoot 內部已經提供了一個默認實現 CaffeineCacheManager(具體可以參見源碼 org.springframework.cache.caffeine.CaffeineCacheManager )。這里不再過多的展開,可以自行閱讀一下源碼了解下~

因此,理想情況下,這就是你所需要的一切了:只需簡單的創建一個 CacheManager 的 bean,就可以為帶 @Cacheable 注釋的方法進行緩存。

到此,我們大概了解了 caffeine 是個什么,以及應該如何用,那么接下來,我們就用示例說話。看不到代碼瞎 BB 也是很讓人討厭的,不是么。

實戰

階段一目標:定義兩個 manager,實現不同的緩存配置。

舉例如下:

@Configuration
public class CaffeineConfig extends CachingConfigurerSupport {

    @Override
    @Bean(name = "cacheManager")
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();

        // 方案一(常用):定制化緩存Cache
        cacheManager.setCaffeine(Caffeine.newBuilder()
                .expireAfterWrite(10, TimeUnit.MINUTES)
                .initialCapacity(100)
                .maximumSize(10_000));

        return cacheManager;
    }

    /**
     * 在@cacheable使用時,指定cacheManager=specCacheManager
     *
     * @return CacheManager
     */
    @Bean(name = "specCacheManager")
    public CacheManager cacheManagerWithSpec() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        // 不允許空值
        cacheManager.setAllowNullValues(false);
        // 傳入一個CaffeineSpec定制緩存,它的好處是可以把配置方便寫在配置文件里
        cacheManager.setCaffeineSpec(CaffeineSpec.parse("initialCapacity=20,maximumSize=100,expireAfterWrite=10m"));

        // 指定使用該策略的CacheNames
        cacheManager.setCacheNames(new ArrayList<String>(Arrays.asList("fetchById", "fetchByName")));

        return cacheManager;
    }
}

  

使用起來也很簡單,畢竟 springBoot 這么牛 x 的框架提供了很好的集成和靈活性。

@Slf4j
@Service
public class UserServiceImpl implements UserService {

    @Override
    @Cacheable(cacheNames = "userSelectOrDefault", cacheManager = "cacheManager", key = "#userId")
    public MyUser selectOrDefault(Integer userId) {

        System.out.println("我要執行【selectOrDefault】方法的查詢邏輯啦~ userId=" + userId);
        System.out.println("當前時間:" + LocalDateTime.now().toString());

        return new MyUser()
                .setUserId(userId)
                .setGender(userId % 2)
                .setUserName("userName_" + userId);
    }

    @Override
    @Cacheable(cacheNames = "fetchByName", cacheManager = "specCacheManager", key = "#userName")
    public MyUser fetchByName(String userName) {
        System.out.println("我要執行【fetchByName】方法的查詢邏輯啦~userName=" + userName);
        System.out.println("當前時間:" + LocalDateTime.now().toString());

        int hashCode = userName.hashCode();

        return new MyUser()
                .setUserId(hashCode)
                .setGender(hashCode % 2)
                .setUserName(userName);
    }
}

  

通過指定 @Cacheable 的 cacheNames、 cacheManager 就可以“靈活”的使用不同的緩存策略了。可能你覺得已經有點小滿足了,畢竟能靈活配置了嘛~~

然鵝~~

冷靜下,真的“靈活”么?

假如一個項目中有很多要緩存(而且也肯定很常見),並且緩存的策略規則也不盡相同時(比如重要的到期時間、初始容量、最大大小等),你是不是就覺得寫很多類似的 cacheManger 有點不爽?

筆者也翻閱了網上一些文章,但大多是告訴你如何使用自定義規范定義自定義緩存。但是,沒有一個實現了我希望的理想狀態。

我期望的是:既可以使用默認的一些策略規范自動創建緩存,又可以靈活的自定義設置你想要的緩存策略。

是不是聽起來有點貪心?其實,我個人覺得這是追求完美的人很正常的一個想法。

畢竟方法總比困那多~

那么,接下來,我們就要把這個想法落地。

階段二:目標:實現一個既可以手動配置,又可以默認的 CacheManger

更多實現細節,請通過移步公眾號~ 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM