Spring Boot 中的多个 TTL 缓存

2021-09-24 10:08:39 浏览数 (3282)

Spring Framework 为常见缓存场景提供了全面的抽象,而无需耦合到任何受支持的缓存实现。但是,特定存储的到期时间声明不是此抽象的一部分。如果我们要设置缓存的生存时间,则必须调整所选缓存提供程序的配置。从这篇文章中,您将学习如何为具有不同 TTL 配置的多个 Caffeine 缓存准备设置

1. 研究案例

让我们从问题的定义开始。我们想象中的应用程序需要缓存两个不同的 REST 端点,但其中一个应该比另一个更频繁地过期。考虑以下外观实现:

@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​注释标记方法Spring的缓存机制。值得一提的是,缓存的方法必须是公开的。每个注解都指定了应该用于特定方法的相应缓存的名称。

缓存实例只不过是一个简单的键值容器。在我们的例子中,键是基于输入参数创建的,值是方法的结果,但它不必那么简单。Spring 提供的缓存抽象允许更多,但这是另一篇文章的主题。如果你对细节感兴趣,我推荐你参考文档。让我们坚持我们的主要目标,即为两个声明的缓存定义不同的 TTL 值。

2. 常用缓存设置

将​@Cacheable​注释放在方法上并不是在应用程序中运行缓存机械化所需的唯一内容。根据所选的提供商,可能会有几个额外的步骤。

2.1. 开启 Spring 缓存

无论您选择哪个提供程序,设置的起点始终是将​@EnableCaching​注释添加到您的配置类之一,通常是主应用程序类。这会在您的 Spring 上下文中注册所有必需的组件。

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

2.2. 必需的依赖项

在使用@EnableCaching注释的常规 Spring 应用程序中,需要开发人员提供​CacheManager​类型的 bean 。幸运的是,Spring Boot 缓存启动器提供了默认管理器,并根据类路径上可用的依赖项创建了一个适当的缓存提供程序,在我们的例子中是 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 支持的大多数缓存提供程序可以使用专用的应用程序属性进行调整。要为演示应用程序所需的两个缓存设置 TTL,我们可以使用以下值:

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

以一种非常简单的方式,我们将缓存的 TTL 设置为 30 分钟,并将它们的容量设置为 100。但是,这种配置的主要问题是所有缓存都使用相同的设置。不可能为每个缓存设置不同的规范。他们都需要共享一个全局的。如果您不介意此类限制,则可以进行基本设置。否则,您应该继续阅读下一部分。

3.区分缓存

Spring Boot 有效地处理流行的配置,但我们的场景不属于这个幸运组。为了根据我们的需要自定义缓存,我们需要超越预定义的 bean 并编写一些自定义初始化代码。

3.1. 自定义缓存管理器

无需禁用 Spring Boot 提供的默认配置,因为我们只能覆盖一个必要的对象。通过定义名为cacheManager的 bean,我们替换了 Spring Boot 提供的 bean 。下面我们创建两个缓存。第一个称为消息,其过期时间等于 30 分钟。另一个名为通知的值存储 60 分钟。当您创建自定义缓存管理器时,application.properties 中的设置(之前在基本示例中介绍过)不再使用,可以安全地删除。

@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 库带有一个方便的缓存构建器。在我们的演示中,我们只关注不同的 TTL 值,但也可以根据需要自定义其他选项(例如容量或访问后非常有用的到期时间)。

在上面的例子中,我们还创建了ticker bean,我们的缓存共享它。自动收报机负责跟踪时间的流逝。实际上,将Ticker类型的实例传递给缓存构建器并不是强制性的,如果没有提供,Caffeine 会创建一个。但是,如果我们想为我们的解决方案编写测试,单独的 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>

如果测试类中存在一个内部静态配置类,则 Spring Boot 1.4.0 中添加的​@SpringBootTest​注释会自动检测并利用内部静态配置类。通过导入主配置类,我们保留了原始项目设置,并仅用假的替换了股票代码实例。

@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;
        }
 
    }
}

我们将在缓存网关类使用的​RestTemplate​实例上使用监控,以观察对真实 REST 端点的可能调用数量。监控应该返回一些存根值以防止实际调用发生。

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));
}

最后,我们可以用我们的快乐路径场景编写一个测试,以确认 TTL 配置是否符合我们的预期。

@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 分钟后,将再次调用​Message​对象。由于消息缓存 TTL 配置为 30 分钟,我们预计将从缓存中获取该值,并且不会调用端点。再过 30 分钟后,我们预计缓存的消息已过期,我们通过对端点的另一次调用来确认这一点。但是,通知缓存已配置为将值保留 60 分钟。通过再次尝试获取通知,我们确认另一个缓存仍然有效。最后,自动收报机再前进 30 分钟,从测试开始算起总共 65 分钟。我们验证通知也已过期并从缓存中删除。

3. 与其他缓存提供者的 TTL

如前所述,Caffeine 的主要缺点是无法区分所有缓存。​spring.cache.caffeine.spec​ 中的规范适用于全球。希望在未来的版本中可以简化多个缓存的设置,但现在我们需要坚持手动配置。

对于其他缓存提供者,幸运的是情况要容易得多。​EhCache​、​Hazelcast ​和 ​Infinitspan​ 使用专用的 XML 配置文件,其中每个缓存都可以单独配置。

4. 总结

尽管 Spring Boot 在为我们解决了平凡的配置方面做得非常出色,但有时我们需要自己做出更好的决定。在简单的情况下,Caffeine 缓存的默认设置可能就足够了,但与其他支持的缓存提供程序相比,它显得相形见绌。阅读这篇文章后,您应该知道如何准备 Caffeine 缓存库的基本和更复杂的自定义配置。