JCache 介紹

且行且码發表於2024-03-17

JCache 是 Java 官方的快取規範即 JSR107,主要明確了Java 中基於記憶體進行物件快取的一些要求,涵蓋物件的建立、查詢、更新、刪除、一致性保證等方面內容;本文主要介紹其基本概念及簡單使用。

1、JCache 簡介

1.1、核心概念

JCache 中定義了五個核心介面:CachingProvider、CacheManager、Cache、Entry 和 ExpiryPolicy。

A、CachingProvider 用於建立、配置、獲取、管理和控制零個或多個 CacheManager;應用程式在執行時可以訪問或使用零個或多個 CachingProvider。
B、CacheManager 用於建立、配置、獲取、管理和控制零個或多個唯一命名的 Cache,這些 Cache 存在於 CacheManager 的上下文中;一個 CacheManager 只能歸屬一個 CachingProvider。
C、Cache 是類似 Map 的資料結構,用於臨時儲存基於 key 的 value;一個 Cache 只能歸屬一個 CacheManager。
D、Entry 是儲存在 Cache 中的鍵值對。
E、ExpiryPolicy 定義了每個 Entry 在 Cache 中有效時間(TTL),有效期內 Entry 能夠被訪問、修改和刪除,有效期後該 Entry 不能在被訪問、修改和刪除。

對應關係如下:

1.2、相關注解

JSR107 將一些常用的 API 方法封裝為註解,利用註解來簡化編碼的複雜度,降低快取對於業務邏輯的侵入性,使得業務開發人員更加專注於業務本身的開發。

註解說明
@CacheResult 將指定的 key 和 value 存入到快取容器中
@CachePut 更新快取容器中對應 key 的快取內容
@CacheRemove 移除快取容器中對應 key 的快取記錄
@CacheRemoveAll 移除快取容器中的所有快取記錄
@CacheKey 指定快取的 key,用於方法引數前面
@CacheValue 指定快取的 value,用於方法引數前面
上述註解主要是新增在方法上面,用於自動將方法的入參與返回結果之間進行一個對映與自動快取,對於後續請求如果命中快取則直接返回快取結果而無需再次執行方法的具體處理,以此來提升介面的響應速度與承壓能力。

1.3、值儲存和引用儲存

JSR-107 中定義了兩種儲存 Entry 的方法,即按值儲存(Store-By-Value)和按引用儲存(Store-By-Reference)。
• 按值儲存是預設機制,也就是在儲存鍵值對時,先對 key 和 value 進行複製,然後將複製的副本儲存到 Cache 中;當訪問 Cache 時,返回的是資料的副本。
• 按引用儲存是另外一種可選的機制,儲存鍵值對時,Cache 中儲存的是 key 和 value 的引用。當應用程式修改鍵值對時,應用程式無需再次修改 Cache 中的資料。

1.4、一致性

一致性是指當併發訪問快取時,需要保證修改的可見性在併發執行緒/程序間是一致的。為了保證一致性,所有的實現框架都應該支援預設一致性模型:

在執行大部分 Cache 操作時,就好像為 Cache 中的每個 key 加了鎖,當某個操作獲取該 key 的排它性讀寫鎖時,後面對該 key 的所有操作都會被阻塞,直到這個鎖釋放。注意,這裡的操作可以是單個 JVM 程序內的,也可以是跨 JVM 的操作。對於某些具有返回值的 Cache 操作,返回的快取值需要是最新值。但是這個最新值,可以根據快取的具體實現定義,比如當併發修改時,這個返回值可以是修改前的值,也可以是修改後的值。

1.5、Cache 和 Map 的差異

相同點:

  • 都是透過 key 進行儲存和訪問。
  • 每個 key 都與一個 value 對應。
  • 當使用可變物件作為 key 時,修改 key 值可能導致獲取不到 key 對應的 value。
  • 使用自定義類作為 key 時,需要覆蓋 Object.equals() 和 Object.hashCode() 方法。

不同點:

  • Cache 的 key 和 value 不許為 null,如果設定了 null,則會丟擲 NullPointerException。
  • Cache 中的 Entry 可能會過期(Expire)。
  • Cache 中的 Entry 可能被驅逐(Evicted)。
  • 為了支援原子比較和交換(compare-and-swap, CAP),Cache 中的自定義 key 應覆蓋 Object.equals() 和 Object.hashCode() 方法。
  • Cache 中的 key 和 value 是需要可被序列化的。
  • Cache 可以設定使用值儲存或引用儲存來儲存 Entry。
  • Cache 可以選擇強制的安全性限制,如果違規操作,可以丟擲 SecurityException 異常。

2、JCache 使用

JCache 只是定義了一組介面,要使用還需引入具體的實現框架。

2.1、Caffeine 中使用 JCache

2.1.1、引入依賴

<dependency>
    <groupId>javax.cache</groupId>
    <artifactId>cache-api</artifactId>
    <version>1.1.1</version>
</dependency>
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>jcache</artifactId>
    <version>2.9.3</version>
</dependency>

2.1.2、簡單使用

@Test
public void caffeine() {
    CachingProvider cachingProvider = Caching.getCachingProvider("com.github.benmanes.caffeine.jcache.spi.CaffeineCachingProvider");
    CacheManager cacheManager = cachingProvider.getCacheManager();
    MutableConfiguration<Integer, String> mutableConfiguration = new MutableConfiguration<Integer, String>()
            .setTypes(Integer.class, String.class)
            .setStoreByValue(false)
            .setExpiryPolicyFactory(CreatedExpiryPolicy.factoryOf(new Duration(TimeUnit.MINUTES, 5)));
    Cache<Integer, String> myCache = cacheManager.createCache("myCache", mutableConfiguration);
    myCache.put(1, "aaa");
    log.info(myCache.get(1));
    cacheManager.close();
}

2.2、Ehcache3 中使用 JCache

2.2.1、引入依賴

<dependency>
    <groupId>javax.cache</groupId>
    <artifactId>cache-api</artifactId>
    <version>1.1.1</version>
</dependency>
<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>3.10.8</version>
    <exclusions>
        <exclusion>
            <groupId>org.glassfish.jaxb</groupId>
            <artifactId>jaxb-runtime</artifactId>
        </exclusion>
    </exclusions>
</dependency>

2.2.2、簡單使用

@Test
public void ehcache3() {
    CachingProvider cachingProvider = Caching.getCachingProvider("org.ehcache.jsr107.EhcacheCachingProvider");
    CacheManager cacheManager = cachingProvider.getCacheManager();
    MutableConfiguration<Integer, String> mutableConfiguration = new MutableConfiguration<Integer, String>()
            .setTypes(Integer.class, String.class)
            .setStoreByValue(false)
            .setExpiryPolicyFactory(CreatedExpiryPolicy.factoryOf(new Duration(TimeUnit.MINUTES, 5)));
    Cache<Integer, String> myCache = cacheManager.createCache("myCache", mutableConfiguration);
    log.info(myCache.getClass().getName() + "|" + myCache.getClass().getCanonicalName());
    myCache.put(1, "aaa");
    log.info(myCache.get(1));
    cacheManager.close();
}

Caffeine 及 Ehcache3 中使用 JCache 的完整程式碼:

JCache 介紹
package com.abc.demo.cache;

import lombok.extern.slf4j.Slf4j;
import org.junit.Test;

import javax.cache.Cache;
import javax.cache.CacheManager;
import javax.cache.Caching;
import javax.cache.configuration.MutableConfiguration;
import javax.cache.expiry.CreatedExpiryPolicy;
import javax.cache.expiry.Duration;
import javax.cache.spi.CachingProvider;
import java.util.concurrent.TimeUnit;

@Slf4j
public class JCacheCase {
    @Test
    public void caffeine() {
        CachingProvider cachingProvider = Caching.getCachingProvider("com.github.benmanes.caffeine.jcache.spi.CaffeineCachingProvider");
        CacheManager cacheManager = cachingProvider.getCacheManager();
        MutableConfiguration<Integer, String> mutableConfiguration = new MutableConfiguration<Integer, String>()
                .setTypes(Integer.class, String.class)
                .setStoreByValue(false)
                .setExpiryPolicyFactory(CreatedExpiryPolicy.factoryOf(new Duration(TimeUnit.MINUTES, 5)));
        Cache<Integer, String> myCache = cacheManager.createCache("myCache", mutableConfiguration);
        myCache.put(1, "aaa");
        log.info(myCache.get(1));
        cacheManager.close();
    }

    @Test
    public void ehcache3() {
        CachingProvider cachingProvider = Caching.getCachingProvider("org.ehcache.jsr107.EhcacheCachingProvider");
        CacheManager cacheManager = cachingProvider.getCacheManager();
        MutableConfiguration<Integer, String> mutableConfiguration = new MutableConfiguration<Integer, String>()
                .setTypes(Integer.class, String.class)
                .setStoreByValue(false)
                .setExpiryPolicyFactory(CreatedExpiryPolicy.factoryOf(new Duration(TimeUnit.MINUTES, 5)));
        Cache<Integer, String> myCache = cacheManager.createCache("myCache", mutableConfiguration);
        log.info(myCache.getClass().getName() + "|" + myCache.getClass().getCanonicalName());
        myCache.put(1, "aaa");
        log.info(myCache.get(1));
        cacheManager.close();
    }
}
JCacheCase.java

2.3、SpringBoot 中 JCache 註解使用

2.3.1、引入依賴

這裡使用 Caffeine 作為 JCache 的實現框架。

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.18</version>
    <relativePath />
</parent>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>

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

2.3.2、配置快取名稱

spring:
  cache:
    type: jcache
    cacheNames: myCache

2.3.3、啟用快取

啟動類上增加 @EnableCaching 註解。

@SpringBootApplication
@EnableCaching
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

2.3.4、JCache 註解使用

package com.abc.general.service.impl;

import com.abc.general.service.ICacheService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.cache.annotation.*;

@Slf4j
@Service
public class CacheServiceImpl implements ICacheService {
    @CacheResult(cacheName = "myCache")
    @Override
    public String queryById(@CacheKey int id) {
        log.info("queryById,id={}", id);
        return "value" + id;
    }

    @CachePut(cacheName = "myCache")
    @Override
    public String updateById(@CacheKey int id, @CacheValue String newValue) {
        log.info("updateById,id={},newValue={}", id, newValue);
        return newValue;
    }

    @CacheRemove(cacheName = "myCache")
    @Override
    public void deleteById(@CacheKey int id) {
        log.info("deleteById,id={}", id);
    }
}

參考:https://developer.aliyun.com/article/786959。