Spring Framework 為常見緩存場(chǎng)景提供了全面的抽象,而無(wú)需耦合到任何受支持的緩存實(shí)現(xiàn)。但是,特定存儲(chǔ)的到期時(shí)間聲明不是此抽象的一部分。如果我們要設(shè)置緩存的生存時(shí)間,則必須調(diào)整所選緩存提供程序的配置。從這篇文章中,您將學(xué)習(xí)如何為具有不同 TTL 配置的多個(gè) Caffeine 緩存準(zhǔn)備設(shè)置。
1. 研究案例
讓我們從問(wèn)題的定義開始。我們想象中的應(yīng)用程序需要緩存兩個(gè)不同的 REST 端點(diǎn),但其中一個(gè)應(yīng)該比另一個(gè)更頻繁地過(guò)期??紤]以下外觀實(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í)例只不過(guò)是一個(gè)簡(jiǎn)單的鍵值容器。在我們的例子中,鍵是基于輸入?yún)?shù)創(chuàng)建的,值是方法的結(jié)果,但它不必那么簡(jiǎn)單。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 緩存
無(wú)論您選擇哪個(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 庫(kù)。
<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
以一種非常簡(jiǎn)單的方式,我們將緩存的 TTL 設(shè)置為 30 分鐘,并將它們的容量設(shè)置為 100。但是,這種配置的主要問(wèn)題是所有緩存都使用相同的設(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. 自定義緩存管理器
無(wú)需禁用 Spring Boot 提供的默認(rèn)配置,因?yàn)槲覀冎荒芨采w一個(gè)必要的對(duì)象。通過(guò)定義名為cacheManager的 bean,我們替換了 Spring Boot 提供的 bean 。下面我們創(chuàng)建兩個(gè)緩存。第一個(gè)稱為消息,其過(guò)期時(shí)間等于 30 分鐘。另一個(gè)名為通知的值存儲(chǔ) 60 分鐘。當(dāng)您創(chuàng)建自定義緩存管理器時(shí),application.properties 中的設(shè)置(之前在基本示例中介紹過(guò))不再使用,可以安全地刪除。
@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 庫(kù)帶有一個(gè)方便的緩存構(gòu)建器。在我們的演示中,我們只關(guān)注不同的 TTL 值,但也可以根據(jù)需要自定義其他選項(xiàng)(例如容量或訪問(wèn)后非常有用的到期時(shí)間)。
在上面的例子中,我們還創(chuàng)建了ticker bean,我們的緩存共享它。自動(dòng)收?qǐng)?bào)機(jī)負(fù)責(zé)跟蹤時(shí)間的流逝。實(shí)際上,將Ticker類型的實(shí)例傳遞給緩存構(gòu)建器并不是強(qiáng)制性的,如果沒(méi)有提供,Caffeine 會(huì)創(chuàng)建一個(gè)。但是,如果我們想為我們的解決方案編寫測(cè)試,單獨(dú)的 bean 將更容易存根。
3.2. TTL緩存測(cè)試
我們?cè)诩蓽y(cè)試中需要的第一件事是一個(gè)帶有假代碼的配置類,它允許模擬時(shí)間流逝。Caffeine 庫(kù)本身不提供這樣的代碼,但文檔中提到了 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)配置類。通過(guò)導(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));
}
最后,我們可以用我們的快樂(lè)路徑場(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)。再過(guò) 30 分鐘后,我們預(yù)計(jì)緩存的消息已過(guò)期,我們通過(guò)對(duì)端點(diǎn)的另一次調(diào)用來(lái)確認(rèn)這一點(diǎn)。但是,通知緩存已配置為將值保留 60 分鐘。通過(guò)再次嘗試獲取通知,我們確認(rèn)另一個(gè)緩存仍然有效。最后,自動(dòng)收?qǐng)?bào)機(jī)再前進(jìn) 30 分鐘,從測(cè)試開始算起總共 65 分鐘。我們驗(yàn)證通知也已過(guò)期并從緩存中刪除。
3. 與其他緩存提供者的 TTL
如前所述,Caffeine 的主要缺點(diǎn)是無(wú)法區(qū)分所有緩存。?spring.cache.caffeine.spec
? 中的規(guī)范適用于全球。希望在未來(lái)的版本中可以簡(jiǎn)化多個(gè)緩存的設(shè)置,但現(xiàn)在我們需要堅(jiān)持手動(dòng)配置。
對(duì)于其他緩存提供者,幸運(yùn)的是情況要容易得多。?EhCache
?、?Hazelcast
?和 ?Infinitspan
? 使用專用的 XML 配置文件,其中每個(gè)緩存都可以單獨(dú)配置。
4. 總結(jié)
盡管 Spring Boot 在為我們解決了平凡的配置方面做得非常出色,但有時(shí)我們需要自己做出更好的決定。在簡(jiǎn)單的情況下,Caffeine 緩存的默認(rèn)設(shè)置可能就足夠了,但與其他支持的緩存提供程序相比,它顯得相形見絀。閱讀這篇文章后,您應(yīng)該知道如何準(zhǔn)備 Caffeine 緩存庫(kù)的基本和更復(fù)雜的自定義配置。