App下載

Spring Boot 中的多個(gè) TTL 緩存

溫柔嘗盡了嗎 2021-09-24 10:08:39 瀏覽數(shù) (3435)
反饋

Spring Framework 為常見緩存場(chǎng)景提供了全面的抽象,而無需耦合到任何受支持的緩存實(shí)現(xiàn)。但是,特定存儲(chǔ)的到期時(shí)間聲明不是此抽象的一部分。如果我們要設(shè)置緩存的生存時(shí)間,則必須調(diào)整所選緩存提供程序的配置。從這篇文章中,您將學(xué)習(xí)如何為具有不同 TTL 配置的多個(gè) Caffeine 緩存準(zhǔn)備設(shè)置。

1. 研究案例

讓我們從問題的定義開始。我們想象中的應(yīng)用程序需要緩存兩個(gè)不同的 REST 端點(diǎn),但其中一個(gè)應(yīng)該比另一個(gè)更頻繁地過期。考慮以下外觀實(shí)現(xiàn):

@Service
class ForeignEndpointGateway {
 
    private RestTemplate restTemplate;
 
    ForeignEndpointGateway(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }
     
    @Cacheable("messages")
    public Message findMessage(long id) {
        String url ="http://somedomain.com/messages/" + id;
        return restTemplate.getForObject(url, Message.class);
    }
 
    @Cacheable("notifications")
    public Notification findNotification(long id) {
        String url ="http://somedomain.com/notifications/" + id;
        return restTemplate.getForObject(url, Notification.class);
    }
 
}

?@Cacheable?注釋標(biāo)記方法Spring的緩存機(jī)制。值得一提的是,緩存的方法必須是公開的。每個(gè)注解都指定了應(yīng)該用于特定方法的相應(yīng)緩存的名稱。

緩存實(shí)例只不過是一個(gè)簡單的鍵值容器。在我們的例子中,鍵是基于輸入?yún)?shù)創(chuàng)建的,值是方法的結(jié)果,但它不必那么簡單。Spring 提供的緩存抽象允許更多,但這是另一篇文章的主題。如果你對(duì)細(xì)節(jié)感興趣,我推薦你參考文檔。讓我們堅(jiān)持我們的主要目標(biāo),即為兩個(gè)聲明的緩存定義不同的 TTL 值。

2. 常用緩存設(shè)置

將?@Cacheable?注釋放在方法上并不是在應(yīng)用程序中運(yùn)行緩存機(jī)械化所需的唯一內(nèi)容。根據(jù)所選的提供商,可能會(huì)有幾個(gè)額外的步驟。

2.1. 開啟 Spring 緩存

無論您選擇哪個(gè)提供程序,設(shè)置的起點(diǎn)始終是將?@EnableCaching?注釋添加到您的配置類之一,通常是主應(yīng)用程序類。這會(huì)在您的 Spring 上下文中注冊(cè)所有必需的組件。

@SpringBootApplication
@EnableCaching
public class TtlCacheApplication {
    // content omitted for clarity
}

2.2. 必需的依賴項(xiàng)

在使用@EnableCaching注釋的常規(guī) Spring 應(yīng)用程序中,需要開發(fā)人員提供?CacheManager?類型的 bean 。幸運(yùn)的是,Spring Boot 緩存啟動(dòng)器提供了默認(rèn)管理器,并根據(jù)類路徑上可用的依賴項(xiàng)創(chuàng)建了一個(gè)適當(dāng)?shù)木彺嫣峁┏绦?,在我們的例子中?Caffeine 庫。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

2.3. 基本配置

Spring Boot 支持的大多數(shù)緩存提供程序可以使用專用的應(yīng)用程序?qū)傩赃M(jìn)行調(diào)整。要為演示應(yīng)用程序所需的兩個(gè)緩存設(shè)置 TTL,我們可以使用以下值:

spring.cache.cache-names=messages,notifications
spring.cache.caffeine.spec=maximumSize=100,expireAfterAccess=1800s

以一種非常簡單的方式,我們將緩存的 TTL 設(shè)置為 30 分鐘,并將它們的容量設(shè)置為 100。但是,這種配置的主要問題是所有緩存都使用相同的設(shè)置。不可能為每個(gè)緩存設(shè)置不同的規(guī)范。他們都需要共享一個(gè)全局的。如果您不介意此類限制,則可以進(jìn)行基本設(shè)置。否則,您應(yīng)該繼續(xù)閱讀下一部分。

3.區(qū)分緩存

Spring Boot 有效地處理流行的配置,但我們的場(chǎng)景不屬于這個(gè)幸運(yùn)組。為了根據(jù)我們的需要自定義緩存,我們需要超越預(yù)定義的 bean 并編寫一些自定義初始化代碼。

3.1. 自定義緩存管理器

無需禁用 Spring Boot 提供的默認(rèn)配置,因?yàn)槲覀冎荒芨采w一個(gè)必要的對(duì)象。通過定義名為cacheManager的 bean,我們替換了 Spring Boot 提供的 bean 。下面我們創(chuàng)建兩個(gè)緩存。第一個(gè)稱為消息,其過期時(shí)間等于 30 分鐘。另一個(gè)名為通知的值存儲(chǔ) 60 分鐘。當(dāng)您創(chuàng)建自定義緩存管理器時(shí),application.properties 中的設(shè)置(之前在基本示例中介紹過)不再使用,可以安全地刪除。

@Bean
public CacheManager cacheManager(Ticker ticker) {
    CaffeineCache messageCache = buildCache("messages", ticker,30);
    CaffeineCache notificationCache = buildCache("notifications", ticker,60);
    SimpleCacheManager manager =new SimpleCacheManager();
    manager.setCaches(Arrays.asList(messageCache, notificationCache));
    return manager;
}
 
private CaffeineCache buildCache(String name, Ticker ticker,int minutesToExpire) {
    return new CaffeineCache(name, Caffeine.newBuilder()
                .expireAfterWrite(minutesToExpire, TimeUnit.MINUTES)
                .maximumSize(100)
                .ticker(ticker)
                .build());
}
 
@Bean
public Ticker ticker() {
    return Ticker.systemTicker();
}

Caffeine 庫帶有一個(gè)方便的緩存構(gòu)建器。在我們的演示中,我們只關(guān)注不同的 TTL 值,但也可以根據(jù)需要自定義其他選項(xiàng)(例如容量或訪問后非常有用的到期時(shí)間)。

在上面的例子中,我們還創(chuàng)建了ticker bean,我們的緩存共享它。自動(dòng)收?qǐng)?bào)機(jī)負(fù)責(zé)跟蹤時(shí)間的流逝。實(shí)際上,將Ticker類型的實(shí)例傳遞給緩存構(gòu)建器并不是強(qiáng)制性的,如果沒有提供,Caffeine 會(huì)創(chuàng)建一個(gè)。但是,如果我們想為我們的解決方案編寫測(cè)試,單獨(dú)的 bean 將更容易存根。

3.2. TTL緩存測(cè)試

我們?cè)诩蓽y(cè)試中需要的第一件事是一個(gè)帶有假代碼的配置類,它允許模擬時(shí)間流逝。Caffeine 庫本身不提供這樣的代碼,但文檔中提到了 guava-testlib,我們需要將其聲明為我們項(xiàng)目的依賴項(xiàng)。

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava-testlib</artifactId>
    <version>20.0</version>
    <scope>test</scope>
</dependency>

如果測(cè)試類中存在一個(gè)內(nèi)部靜態(tài)配置類,則 Spring Boot 1.4.0 中添加的?@SpringBootTest?注釋會(huì)自動(dòng)檢測(cè)并利用內(nèi)部靜態(tài)配置類。通過導(dǎo)入主配置類,我們保留了原始項(xiàng)目設(shè)置,并僅用假的替換了股票代碼實(shí)例。

@RunWith(SpringRunner.class)
@SpringBootTest
public class MessageRepositoryTest {
 
    @Configuration
    @Import(TtlCacheApplication.class)
    public static class TestConfig {
 
        static FakeTicker fakeTicker =new FakeTicker();
 
        @Bean
        public Ticker ticker() {
            return fakeTicker::read;
        }
 
    }
}

我們將在緩存網(wǎng)關(guān)類使用的?RestTemplate?實(shí)例上使用監(jiān)控,以觀察對(duì)真實(shí) REST 端點(diǎn)的可能調(diào)用數(shù)量。監(jiān)控應(yīng)該返回一些存根值以防止實(shí)際調(diào)用發(fā)生。

private static final long MESSAGE_ID =1;
private static final long NOTIFICATION_ID =2;
 
@SpyBean
private RestTemplate restTemplate;
@Autowired
private ForeignEndpointGateway gateway;
 
@Before
public void setUp()throws Exception {
    Message message = stubMessage(MESSAGE_ID);
    Notification notification = stubNotification(NOTIFICATION_ID);
    doReturn(message)
            .when(restTemplate)
            .getForObject(anyString(), eq(Message.class));
    doReturn(notification)
            .when(restTemplate)
            .getForObject(anyString(), eq(Notification.class));
}

最后,我們可以用我們的快樂路徑場(chǎng)景編寫一個(gè)測(cè)試,以確認(rèn) TTL 配置是否符合我們的預(yù)期。

@Test
public void shouldUseCachesWithDifferentTTL()throws Exception {
    // 0 minutes
    foreignEndpointGateway.findMessage(MESSAGE_ID);
    foreignEndpointGateway.findNotification(NOTIFICATION_ID);
    verify(restTemplate, times(1)).getForObject(anyString(), eq(Message.class));
    verify(restTemplate, times(1)).getForObject(anyString(), eq(Notification.class));
    // after 5 minutes
    TestConfig.fakeTicker.advance(5, TimeUnit.MINUTES);
    foreignEndpointGateway.findMessage(MESSAGE_ID);
    verify(restTemplate, times(1)).getForObject(anyString(), eq(Message.class));
    // after 35 minutes
    TestConfig.fakeTicker.advance(30, TimeUnit.MINUTES);
    foreignEndpointGateway.findMessage(MESSAGE_ID);
    foreignEndpointGateway.findNotification(NOTIFICATION_ID);
    verify(restTemplate, times(2)).getForObject(anyString(), eq(Message.class));
    verify(restTemplate, times(1)).getForObject(anyString(), eq(Notification.class));
    // after 65 minutes
    TestConfig.fakeTicker.advance(30, TimeUnit.MINUTES);
    foreignEndpointGateway.findNotification(NOTIFICATION_ID);
    verify(restTemplate, times(2)).getForObject(anyString(), eq(Notification.class));
}

一開始,?Message?和?Notification?對(duì)象都是從端點(diǎn)獲取并放置在緩存中。5 分鐘后,將再次調(diào)用?Message?對(duì)象。由于消息緩存 TTL 配置為 30 分鐘,我們預(yù)計(jì)將從緩存中獲取該值,并且不會(huì)調(diào)用端點(diǎn)。再過 30 分鐘后,我們預(yù)計(jì)緩存的消息已過期,我們通過對(duì)端點(diǎn)的另一次調(diào)用來確認(rèn)這一點(diǎn)。但是,通知緩存已配置為將值保留 60 分鐘。通過再次嘗試獲取通知,我們確認(rèn)另一個(gè)緩存仍然有效。最后,自動(dòng)收?qǐng)?bào)機(jī)再前進(jìn) 30 分鐘,從測(cè)試開始算起總共 65 分鐘。我們驗(yàn)證通知也已過期并從緩存中刪除。

3. 與其他緩存提供者的 TTL

如前所述,Caffeine 的主要缺點(diǎn)是無法區(qū)分所有緩存。?spring.cache.caffeine.spec? 中的規(guī)范適用于全球。希望在未來的版本中可以簡化多個(gè)緩存的設(shè)置,但現(xiàn)在我們需要堅(jiān)持手動(dòng)配置。

對(duì)于其他緩存提供者,幸運(yùn)的是情況要容易得多。?EhCache?、?Hazelcast ?和 ?Infinitspan? 使用專用的 XML 配置文件,其中每個(gè)緩存都可以單獨(dú)配置。

4. 總結(jié)

盡管 Spring Boot 在為我們解決了平凡的配置方面做得非常出色,但有時(shí)我們需要自己做出更好的決定。在簡單的情況下,Caffeine 緩存的默認(rèn)設(shè)置可能就足夠了,但與其他支持的緩存提供程序相比,它顯得相形見絀。閱讀這篇文章后,您應(yīng)該知道如何準(zhǔn)備 Caffeine 緩存庫的基本和更復(fù)雜的自定義配置。

0 人點(diǎn)贊