一個緩存使用的思考:Spring Cache VS Caffeine 原生 API


歡迎訪問我的個人博客,《一個緩存使用的思考:Spring Cache VS Caffeine 原生 API》

最近在學習本地緩存發現,在 Spring 技術棧的開發中,既可以使用 Spring Cache 的注解形式操作緩存,也可用各種緩存方案的原生 API。那么是否 Spring 官方提供的就是最合適的方案呢?那么本文將通過一個案例來為你揭曉。

Spring Cache

Since version 3.1, the Spring Framework provides support for transparently adding caching to an existing Spring application. The caching abstraction allows consistent use of various caching solutions with minimal impact on the code.

Spring Cache 和 slf4j、jdbc 類似,是由 Spring Framwork 提供的一個緩存抽象層,可以接入各種緩存解決方案來進行使用,通過 Spring Cache 的集成,我們只需要通過一組注解來操作緩存就可以了。目前支持的有 GenericJCache (JSR-107)EhCache 2.xHazelcastInfinispanCouchbaseRedisCaffeineSimple,幾乎包含了主流的本地緩存方案。

其主要的原理就是向 Spring Context 中注入 Cache 和 CacheManager 這兩個 bean,再通過 Spring Boot 的自動裝配技術,會根據項目中的配置文件自動注入合適的 Cache 和 CacheManager 實現。

本地緩存方案

Java 技術棧中成熟的本地緩存方案已經有很多了,有大而全的 ehcache,也有后起之秀 Google Guava Cache。下面是常用的三大本地緩存方案的對比,引用自博客 如何優雅的設計和使用緩存?

項目 Ehcache Guava Cache Caffeine
讀寫性能 好,需要做淘汰操作 很好
淘汰算法 支持多種淘汰算法, LRU,LFU,FIFO LRU,一般 W-TinyLFU, 很好
功能豐富程度 功能很豐富 功能很豐富,支持刷新和虛引用等 功能和 Guava Cache 類似
工具大小 很大,最新版本 1.4MB 是 Guava 工具類中的一個小部分,較小 一般,最新版本 644KB
是否持久化
是否支持集群

目前比較推薦的是 Caffeine,淘汰算法比較先進,並且得到 Spring Cache 的支持(新版的 Spring Cache 不再支持 Guava Cache)。下文的代碼也是使用 Caffeine 的原生 API 的。

案例

使用過 Spring Cache 的人應該會發現,通過幾個注解就能夠輕松實現緩存的 CRUD 操作,並且替換其他的緩存方案不需要對代碼進行改動嗎,同時也不需要寫例如下文的樣板代碼:

{
    // 緩存命中
    if(cache.getIfPresent(key) != null){
        // todo
    }else{
        // 緩存未命中,IO 獲取數據,結果存入緩存
        Object value = repo.getFromDB(key);
        cache.put(key,value);
    }
}

那學到這里,我就產生了疑惑,既然 Spring 出了緩存的注解化開發,並且大量的博客也都在往 Spring Cache 上引,那還是否需要用原生 API 呢?畢竟在 Spring Data JPA 出現后,我們的確很少關注后端 ORM 框架,也不再直接使用 Hibernate 了。

當我實現了項目中的一個需求,這個問題好像就豁然開朗了。

其實需求很簡單,原本在本地 HashMap 中維護的一個映射表,由於后期需要頻繁改動而放到了數據庫中。但由於數據量並不大且不配置映射表時,數據保持不變,因此既然在學習緩存,就想把它加進去。那么現在需要做的就是:

  1. 一個讀取映射表全表的方法 aliasMap()。並緩存數據到 Caffeine。
  2. 一個支持映射記錄 CRUD 操作的頁面,且修改映射表時,更新緩存。
@Cacheable(value = "default", key = "#root.methodName")
@Override
public Map<String, String> aliasMap() {
	return getMapFromDB();
}

由於 Spring Cache 的注解一般是添加在類或者方法上的,換而言之,緩存的是方法返回的對象。顯然,通過某個方法來觸發另一個緩存中的對象的更新是行不通的。這樣是否意味着 Spring Cache 無法實現了呢?仔細去看一下 Spring Cache 的原理,其實還是可行的。

Spring Cache 會向 Spring Context 中注入 Cache 和 CacheManager 這兩個 bean,再通過 Spring Boot 的自動裝配技術,根據項目中的配置文件自動注入合適的 Cache 和 CacheManager 實現。再看到 CaffeineCacheManager 的源碼:

public class CaffeineCacheManager implements CacheManager {
    private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap(16);
    private boolean dynamic = true;
    private Caffeine<Object, Object> cacheBuilder = Caffeine.newBuilder();
    @Nullable
    private CacheLoader<Object, Object> cacheLoader;
    private boolean allowNullValues = true;
}

顯然,緩存是存在 cacheMap 這樣一個 ConcurrentHashMap 中,那只要我們能夠手動去獲取到這個 bean 的實例去操作它,那么這個需求就可以實現了,代碼如下:

@Autowired
private CacheManager cacheManager;
@Cacheable(value = "default", key = "#root.methodName")
@Override
public Map<String, String> aliasMap() {
    return getMapFromDB();
}

private Map<String, String> getMapFromDB() {
    Map<String, String> map = new HashMap<>();
    List<PartAlias> list = repository.findAll();
    list.forEach(x -> map.put(x.getAlias(), x.getName()));
    return map;
}

@Override
public PartAlias saveOrUpdateWithCache(PartAlias obj) {
    PartAlias partAlias = repository.saveAndFlush(obj);
    Cache cache = cacheManager.getCache("default");
    cache.clear();
    cache.put("aliasMap", getMapFromDB());
    return partAlias;
}

經過測試,上面的代碼是可行的。顯然,遇到一些稍微復雜的需求,僅僅依靠 Spring Cache 的注解是遠遠不夠的,我們需要自己去操作 cache 對象。如果使用原生 API 就非常簡單了,能應對不同的需求。

What's More

上面的需求,Spring Cache 尚且還是能夠處理的,但是如果要實現數據的自動加載和刷新呢?現在 Spring Cache 並不能夠很好的支持。

spring:
  cache:
    type: caffeine
    caffeine:
      spec: maximumSize=1024
    cache-names: cache1,cache2

上面的代碼是用來配置 cache 的,結合上文 CaffeineCacheManager 的源碼,我們可以知道,Spring Cache 的配置是全局的,也就是說例如最大條數、過期時間等參數是為全體緩存進行設置的,無法單獨為某個緩存設置。而在 Caffeine 中用於數據加載和刷新的 CacheLoader 也是 CaffeineCacheManager 這個 bean 共有的,因此也就失去存在的意義,畢竟每個緩存的加載和數據刷新的方式是不可能相同的。

因此,在遇到復雜場景下, 還是得上原生 API 的,Spring Cache 就顯得心有余而力不足了。筆者也寫個一個工具類,可以全局使用緩存。

@Component
public class CaffeineCacheManager {
    private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>(16);

    /**
     * 緩存創建
     *
     * @param cacheName
     * @param cache
     */
    public void createCache(String cacheName, Cache cache) {
        cacheMap.put(cacheName, cache);
    }

    /**
     * 緩存獲取
     *
     * @param name
     * @return
     */
    public synchronized Cache getCache(String name) {
        Cache cache = this.cacheMap.get(name);
        if (cache == null) {
            throw new IllegalArgumentException("No this cache.");
        }
        return cache;
    }

    @Autowired
    private static CaffeineCacheManager manager;
    public static void main(String[] args) {
        manager.createCache("default", Caffeine.newBuilder()
                .maximumSize(1024)
                .build());
        Cache<String, Object> cache = manager.getCache("default");
        // TODO
    }
}

當然,再來提一提,既然是 Spring 的套路,總是會給開發者留一條后路的,如果願意折騰的,可以閱讀 CacheManager 的代碼,再根據自己需求重新實現,從而管理自己的 cache 實例。

總結

本文不是一篇介紹 Spring Cache 和 Caffeine 用法的文章(有需要可以閱讀參考文獻),而是在探討 Spring Cache 和 Caffeine 的原生 API 的使用場景。顯然,Spring 全家桶有時未必是最優的解決方案(有能力重寫的另當別論了)!所以也希望網上有更多的博客可以 focus on 框架本身的使用,而不是千篇一律的各種集成到 Spring xxx。

附錄

yaml 配置

initialCapacity: # 初始的緩存空間大小
maximumSize: # 緩存的最大條數
maximumWeight: # 緩存的最大權重
expireAfterAccess: # 最后一次寫入或訪問后經過固定時間過期
expireAfterWrite: # 最后一次寫入后經過固定時間過期
refreshAfterWrite: # 創建緩存或者最近一次更新緩存后經過固定的時間間隔,刷新緩存
weakKeys: # 打開 key 的弱引用
weakValues:  # 打開 value 的弱引用
softValues: # 打開 value 的軟引用
recordStats: # 開發統計功能

原理篇

合理使用緩存

緩存的目的主要是為了降低主主數據庫的壓力,服務可以直接從緩存中獲取數據,從而提高響應速度,讓原本有限的資源可以服務更多的用戶。

從工程的角度來說,緩存的引入並不是盲目的,如果主數據庫壓力不大的情況,並不需要添加緩存。多添加一個數據中間件顯然也會增加維護的成本,而且在實際使用過程中還會存在一些,例如緩存擊穿、緩存雪崩等問題。

基本概念

  • 命中率。 返回正確結果數 / 請求緩存次數, 命中率越高,表明緩存的使用率越高。
  • 最大元素。緩存中可以存放元素的最大數目, 一旦超過,會通過合適的策略進行清空操作。
  • 清空策略:FIFO、LFU、LRU

緩存類型

緩存根據存儲的方式可以分成本地緩存和分布式緩存。

  • 本地緩存:本地緩存一般指的是緩存在應用進程內部的緩存。以 Java 技術棧為例,可是自己實現一個 HashMap 作為數據緩存,也可以直接使用現成的緩存方案,例如 ehcache、caffeine 等。
  • 分布式緩存:緩存和應用環境分離,會單獨存放在自己的服務器或集群里,且多個應用可直接的共享緩存。 常見的緩存方案有 MemCache 和 Redis 等。

這一節主要是讓大家對緩存有一個基本的認識,緩存不是一種具體的技術,而是一種通用的技術方案,如何選擇合適的緩存方案集成到自己的項目中去並且如何解決引入緩存后產生的一些經典問題,不是本文討論的重點。有關於緩存的詳細介紹和選型可以參考:

參考文獻


免責聲明!

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



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