Spring Cache + Caffeine的整合與使用

Scotyzh發表於2023-12-14

前言

對於一些專案裡需要對資料庫裡的某些資料一直重複請求的,且這些資料基本是固定的,在這種情況下,可以藉助簡單使用本地快取來快取這些資料。這些介紹一下Spring Cache和Caffeine的使用。

引入依賴和CacheConfig

在pom檔案裡面引入下面的依賴:

    <dependency>
      <groupId>com.github.ben-manes.caffeine</groupId>
      <artifactId>caffeine</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>

在啟動類上加上@EnableCaching的註解

@EnableCaching
public class SpringBootApplication{

}

新建一個CacheConfig類

@Configuration
public class CacheConfig {

    /********************************
     *  @function  : 生成快取管理器
     *  @parameter : []
     *  @return    : org.springframework.cache.CacheManager
     *  @date      : 2023/12/13 14:46
     ********************************/
    @Primary
    @Bean("customCacheManager")
    public CacheManager customCacheManager() {
        SimpleCacheManager simpleCacheManager = new SimpleCacheManager();
        List<Cache> cacheList = new ArrayList<>();
        cacheList.add(customCache());
        simpleCacheManager.setCaches(cacheList);
        return simpleCacheManager;
    }

    /********************************
     *  @function  : 生成自定義快取容器
     *  @parameter : []
     *  @return    : org.springframework.cache.Cache
     *  @date      : 2023/12/13 14:46
     ********************************/
    public Cache customCache() {
        return new CaffeineCache("customCache", Caffeine.newBuilder()
                .build(), true);
    }
}

這裡customCache()方法我並沒有設定相關過期時間和最大值,不設定會導致沒有預設過期時間和最大值。如果需要設定可以參考下面的寫法

    public Cache customCache() {
        return new CaffeineCache("customCache", Caffeine.newBuilder()
                .maximumSize(100)
                .initialCapacity(100)
                .expireAfterWrite(10, TimeUnit.MINUTES)
                .recordStats()
                .build(),
                true);
    }

CaffeineCache引數的講解

  1. "customCache": 這是快取的名稱。在應用程式中,你可以透過這個名稱來獲取對應的快取例項。
  2. Caffeine.newBuilder(): 這是建立Caffeine快取例項的起始點。newBuilder()返回一個Caffeine構建器物件,用於配置和定製快取的各種屬性。
  3. .maximumSize(100): 這是設定快取的最大容量,即快取可以容納的最大條目數。在這個例子中,快取的最大容量被設定為100。
  4. .initialCapacity(100): 這是設定快取的初始容量,即在快取初始化時分配的內部資料結構的初始大小。在這個例子中,初始容量被設定為100。
  5. .expireAfterWrite(10, TimeUnit.MINUTES): 這是設定快取項在被寫入後的過期時間。在這個例子中,快取項將在被寫入後的10分鐘內過期。
  6. .recordStats(): 這是啟用快取統計資訊的選項。啟用後,你可以從快取例項中獲取有關快取使用情況的統計資訊,例如命中率、載入次數等。

使用中,對過期策略的使用會比較重要,對於過期的策略有:

  1. 寫入後過期 (expireAfterWrite): 快取項被寫入後的一段時間內過期。可以透過以下方式配置:

    Caffeine.newBuilder()
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .build();
    

    在上述示例中,快取項將在被寫入後的10分鐘內過期。

  2. 訪問後過期 (expireAfterAccess): 快取項在一段時間內沒有被訪問後過期。可以透過以下方式配置:

    Caffeine.newBuilder()
            .expireAfterAccess(15, TimeUnit.MINUTES)
            .build();
    

    在上述示例中,快取項將在最後一次訪問後的15分鐘內過期。

  3. 定時過期 (expireAfter): 快取項在指定的固定時間內過期,不考慮寫入或訪問。可以透過以下方式配置:

    Caffeine.newBuilder()
            .expireAfter(1, TimeUnit.HOURS)
            .build();
    

    在上述示例中,快取項將在建立後的1小時內過期。

這些過期定時策略可以根據具體的使用場景和需求進行組合或選擇。

上面不同寫法將會導致生成不同的localcache實現類,可以在build方法中看到:

image-20231213211003206

進入isBounded()方法:

image-20231213211041692

如果使用快取會呼叫localcache的get方法,最後進入computeIfAbsent()方法,對比上面兩個實現類的實現,先是BoundedLocalCache:

image-20231213211342736

UnboundedLocalCache:

image-20231213211443944

下面這個並不會去檢查是否過期。

使用示範

在MVC的使用,可以將快取的註解標識於service層:

@Service
@Slf4j
@CacheConfig(cacheNames = "customCache", cacheManager = "customCacheManager")
public class CalDataInitServiceImpl implements ICalDataInitService {

    @Cacheable(key = "#root.methodName + #sendCardName")
    public int getSlotCount(String sendCardName) {
        ..方法體
        return calCard.getSlotCount();
    }
    
    ...
        
	@CachePut(key = "#param")
	public String updateCache(String param) {
    	// 對資料庫更新某個值
  	   return updatedValue;
	}

    @CacheEvict(key = "#param")
    public void evictCache(String param) {
        // 對資料庫刪除某個值
    }
    
}

使用到的註解解析:

@CacheConfig(cacheNames = "customCache", cacheManager = "customCacheManager") 標註在類上,cacheNames表示當前使用的快取名字,在建立快取的時候有指定,第二個cacheManager是建立cacheManager管理器時指定的Bean名稱,這裡是 @Bean("customCacheManager")。

@Cacheable 是Spring框架中用於宣告快取規則的註解之一。它通常用於標記在方法上,以指示Spring在執行方法前先檢查快取,如果快取中已有資料,則直接返回快取中的資料,而不執行方法體。如果快取中沒有資料,則執行方法體,並將方法的返回值存入快取。

以下是 以@Cacheable 註解為例的主要引數介紹和使用方式:

  1. value(或 cacheNames): 指定快取的名稱,可以指定一個或多個快取。如果指定多個快取,Spring會依次檢查快取,直到找到第一個有資料的快取或全部檢查完畢。示例:

    @Cacheable(value = "myCache")
    public String getCachedData() {
        // 方法體
    }
    
  2. key 指定快取項的鍵。預設情況下,Spring會使用方法的引數作為鍵,但你也可以透過 key 屬性指定自定義的快取鍵。示例:

    @Cacheable(value = "myCache", key = "#param")
    public String getCachedData(String param) {
        // 方法體
    }
    
  3. condition 指定條件表示式,只有當條件滿足時才會快取。示例:

    @Cacheable(value = "myCache", condition = "#result != null")
    public String getCachedData() {
        // 方法體
    }
    
  4. unless 指定一個條件表示式,當條件為 true 時,不會將結果放入快取。示例:

    @Cacheable(value = "myCache", unless = "#result == null")
    public String getCachedData() {
        // 方法體
    }
    
  5. keyGenerator 指定自定義的快取鍵生成器。這個屬性允許你提供一個實現了 org.springframework.cache.interceptor.KeyGenerator 介面的類,用於生成快取鍵。示例:

    @Cacheable(value = "myCache", keyGenerator = "customKeyGenerator")
    public String getCachedData() {
        // 方法體
    }
    
  6. sync 是否啟用同步模式。如果設定為 true,可以解決併發查的問題,Spring會在呼叫方法時鎖定快取,防止多個執行緒同時訪問資料庫。預設為 false。示例:

    @Cacheable(value = "myCache", sync = true)
    public String getCachedData() {
        // 方法體
    }
    

這些是 @Cacheable 註解的一些常用引數。可以根據實際需要選擇合適的引數來定義快取規則。

在Spring中,除了 @Cacheable,另外一些註解及其簡要介紹:

  1. @CacheEvict 用於從快取中移除資料。通常用於在方法執行後清空指定快取。示例:

    @CacheEvict(value = "myCache", key = "#param")
    public void evictCache(String param) {
        // 方法體
    }
    
  2. @CachePut 用於將方法的返回值更新到快取中,常用於更新快取而不影響方法的執行。示例:

    @CachePut(value = "myCache", key = "#param")
    public String updateCache(String param) {
        // 方法體
        return updatedValue;
    }
    
  3. @Caching 用於將多個快取相關的註解組合在一起,實現複雜的快取操作。示例:

    @Caching(
        evict = {@CacheEvict(value = "cache1", key = "#param1")},
        put = {@CachePut(value = "cache2", key = "#param2")}
    )
    public String complexCacheOperation(String param1, String param2) {
        // 方法體
    }
    
  4. @CacheConfig 用於在類級別配置快取的一些公共屬性,避免在每個方法上都重複指定相同的快取名稱等資訊。示例:

    @CacheConfig(cacheNames = "commonCache")
    public class MyService {
        @Cacheable
        public String getCachedData(String param) {
            // 方法體
        }
    }
    

這些註解可以單獨使用,也可以結合使用,以滿足不同的快取需求。

清空快取的方法

清空所有快取,可以不指定 valuekey,如下所示:

@CacheEvict(allEntries = true)
public void evictAllCaches() {
    // 方法體
}

在這個例子中,allEntries = true 表示清空所有快取。

如果你想根據某個條件來判斷是否清空快取,可以使用 condition 屬性,例如:

@CacheEvict(value = "myCache", key = "#param", condition = "#param != 'noEviction'")
public void evictCacheConditionally(String param) {
    // 方法體
}

在上述例子中,只有當 param 不等於 'noEviction' 時才會執行快取清空操作。

除了 @CacheEvict,在一些特定場景下,@CachePut 也可以被用來“清空”快取,因為它將方法的返回值放入快取,如果返回值為 null,相當於移除快取項。這種方式通常在更新操作時使用。

注意事項

如下圖程式碼所示,如果在updateCache方法又呼叫了同個類裡面的getSlotCount()方法,是不會使用到快取的,這是因為快取的實現是透過AOP實現,在同個類裡面呼叫方法,實際是透過this來調,不會呼叫到代理物件,因此相當於@Cacheable註解在這種情況是不生效的。

@Service
@Slf4j
@CacheConfig(cacheNames = "customCache", cacheManager = "customCacheManager")
public class CalDataInitServiceImpl implements ICalDataInitService {

    @Cacheable(key = "#root.methodName + #sendCardName")
    public int getSlotCount(String sendCardName) {
        ..方法體
        return calCard.getSlotCount();
    }
    
    ...
        
	@CachePut(key = "#param")
	public String updateCache(String param) {
        getSlotCount("xx");
    	// 對資料庫更新某個值
  	   return updatedValue;
	}

    
}

相關文章