App下載

Spring Boot 中的多個 TTL 緩存

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

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

1. 研究案例

讓我們從問題的定義開始。我們想象中的應(yīng)用程序需要緩存兩個不同的 REST 端點,但其中一個應(yīng)該比另一個更頻繁地過期。考慮以下外觀實現(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的緩存機制。值得一提的是,緩存的方法必須是公開的。每個注解都指定了應(yīng)該用于特定方法的相應(yīng)緩存的名稱。

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

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

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

2.1. 開啟 Spring 緩存

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

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

2.2. 必需的依賴項

在使用@EnableCaching注釋的常規(guī) Spring 應(yīng)用程序中,需要開發(fā)人員提供?CacheManager?類型的 bean 。幸運的是,Spring Boot 緩存啟動器提供了默認(rèn)管理器,并根據(jù)類路徑上可用的依賴項創(chuàng)建了一個適當(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)用程序所需的兩個緩存設(shè)置 TTL,我們可以使用以下值:

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

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

3.區(qū)分緩存

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

3.1. 自定義緩存管理器

無需禁用 Spring Boot 提供的默認(rèn)配置,因為我們只能覆蓋一個必要的對象。通過定義名為cacheManager的 bean,我們替換了 Spring Boot 提供的 bean 。下面我們創(chuàng)建兩個緩存。第一個稱為消息,其過期時間等于 30 分鐘。另一個名為通知的值存儲 60 分鐘。當(dāng)您創(chuàng)建自定義緩存管理器時,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òu)建器。在我們的演示中,我們只關(guān)注不同的 TTL 值,但也可以根據(jù)需要自定義其他選項(例如容量或訪問后非常有用的到期時間)。

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

3.2. TTL緩存測試

我們在集成測試中需要的第一件事是一個帶有假代碼的配置類,它允許模擬時間流逝。Caffeine 庫本身不提供這樣的代碼,但文檔中提到了 guava-testlib,我們需要將其聲明為我們項目的依賴項。

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

如果測試類中存在一個內(nèi)部靜態(tài)配置類,則 Spring Boot 1.4.0 中添加的?@SpringBootTest?注釋會自動檢測并利用內(nèi)部靜態(tài)配置類。通過導(dǎo)入主配置類,我們保留了原始項目設(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?實例上使用監(jiān)控,以觀察對真實 REST 端點的可能調(diào)用數(shù)量。監(jiān)控應(yīng)該返回一些存根值以防止實際調(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));
}

最后,我們可以用我們的快樂路徑場景編寫一個測試,以確認(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?對象都是從端點獲取并放置在緩存中。5 分鐘后,將再次調(diào)用?Message?對象。由于消息緩存 TTL 配置為 30 分鐘,我們預(yù)計將從緩存中獲取該值,并且不會調(diào)用端點。再過 30 分鐘后,我們預(yù)計緩存的消息已過期,我們通過對端點的另一次調(diào)用來確認(rèn)這一點。但是,通知緩存已配置為將值保留 60 分鐘。通過再次嘗試獲取通知,我們確認(rèn)另一個緩存仍然有效。最后,自動收報機再前進(jìn) 30 分鐘,從測試開始算起總共 65 分鐘。我們驗證通知也已過期并從緩存中刪除。

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

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

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

4. 總結(jié)

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

0 人點贊