引言
本文将解释Spring WebFlux如何与@Cacheable
注解交互。首先,我们将讨论一些常见问题以及如何避免它们。接下来,我们会介绍可用的解决方法。最后,如往常一样,我们提供代码示例。
2. @Cacheable
与反应式类型
这个主题相对较新。撰写本文时,@Cacheable
与反应式框架之间还没有流畅的集成。主要问题是,没有非阻塞缓存实现(JSR-107缓存API是阻塞的)。只有Redis提供了反应式驱动器。
尽管我们在上一段提到的问题,我们仍然可以在服务方法上使用@Cacheable
。这会导致我们的包装对象(Mono
或Flux
)被缓存,但不会缓存方法的实际结果。
2.1. 项目设置
让我们通过一个测试来说明这一点。在测试之前,我们需要设置项目。我们将创建一个简单的Spring WebFlux项目,并使用反应式MongoDB驱动器。我们将不单独运行MongoDB进程,而是使用Testcontainers。
我们的测试类将使用@SpringBootTest
注解,并包含以下内容:
final static MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:4.0.10"));
@DynamicPropertySource
static void mongoDbProperties(DynamicPropertyRegistry registry) {
mongoDBContainer.start();
registry.add("spring.data.mongodb.uri", mongoDBContainer::getReplicaSetUrl);
}
这些行将启动一个MongoDB实例,并将URI传递给SpringBoot,以便自动配置Mongo库。
为了这个测试,我们将创建ItemService
类,包含save
和getItem
方法:
@Service
public class ItemService {
private final ItemRepository repository;
public ItemService(ItemRepository repository) {
this.repository = repository;
}
@Cacheable("items")
public Mono<Item> getItem(String id){
return repository.findById(id);
}
public Mono<Item> save(Item item){
return repository.save(item);
}
}
在application.properties
中,我们设置了日志记录器,以便在测试中监控发生的情况:
logging.level.org.springframework.data.mongodb.core.ReactiveMongoTemplate=DEBUG
logging.level.org.springframework.cache=TRACE
2.2. 初始测试
设置完成后,我们可以运行测试并分析结果:
@Test
public void givenItem_whenGetItemIsCalled_thenMonoIsCached() {
Mono<Item> glass = itemService.save(new Item("glass", 1.00));
String id = glass.block().get_id();
Mono<Item> mono = itemService.getItem(id);
Item item = mono.block();
assertThat(item).isNotNull();
assertThat(item.getName()).isEqualTo("glass");
assertThat(item.getPrice()).isEqualTo(1.00);
Mono<Item> mono2 = itemService.getItem(id);
Item item2 = mono2.block();
assertThat(item2).isNotNull();
assertThat(item2.getName()).isEqualTo("glass");
assertThat(item2.getPrice()).isEqualTo(1.00);
}
在控制台中,我们可以看到以下输出(为了简洁,只显示了必要的部分):
Inserting Document containing fields: [name, price, _class] in collection: item...
Computed cache key '618817a52bffe4526c60f6c0' for operation Builder[public reactor.core.publisher.Mono...
No cache entry for key '618817a52bffe4526c60f6c0' in cache(s) [items]
Computed cache key '618817a52bffe4526c60f6c0' for operation Builder[public reactor.core.publisher.Mono...
findOne using query: { "_id" : "618817a52bffe4526c60f6c0"} fields: Document{{}} for class: class com.baeldung.caching.Item in collection: item...
findOne using query: { "_id" : { "$oid" : "618817a52bffe4526c60f6c0"}} fields: {} in db.collection: test.item
Computed cache key '618817a52bffe4526c60f6c0' for operation Builder[public reactor.core.publisher.Mono...
Cache entry for key '618817a52bffe4526c60f6c0' found in cache 'items'
findOne using query: { "_id" : { "$oid" : "618817a52bffe4526c60f6c0"}} fields: {} in db.collection: test.item
第一行是我们看到的插入方法。然后,在调用getItem
时,Spring检查缓存中是否有此项,但未找到,因此访问MongoDB获取这条记录。第二次调用getItem
时,Spring再次检查缓存并找到了该键的条目,但仍去MongoDB获取记录。
这是因为Spring缓存了getItem
方法的结果,即Mono
包装对象。然而,对于实际结果,它仍然需要从数据库中获取记录。
在接下来的章节中,我们将提供解决这个问题的方法。
3. 缓存Mono/Flux
的结果
Mono
和Flux
具有内置的缓存机制,我们可以在这种情况下使用它作为工作绕过。如前所述,@Cacheable
缓存包装对象,而使用内置缓存,我们可以创建对服务方法实际结果的引用:
@Cacheable("items")
public Mono<Item> getItem_withCache(String id) {
return repository.findById(id).cache();
}
让我们运行上一章中的测试,使用新的服务方法。输出将如下所示:
Inserting Document containing fields: [name, price, _class] in collection: item
Computed cache key '6189242609a72e0bacae1787' for operation Builder[public reactor.core.publisher.Mono...
No cache entry for key '6189242609a72e0bacae1787' in cache(s) [items]
Computed cache key '6189242609a72e0bacae1787' for operation Builder[public reactor.core.publisher.Mono...
findOne using query: { "_id" : "6189242609a72e0bacae1787"} fields: Document{{}} for class: class com.baeldung.caching.Item in collection: item
findOne using query: { "_id" : { "$oid" : "6189242609a72e0bacae1787"}} fields: {} in db.collection: test.item
Computed cache key '6189242609a72e0bacae1787' for operation Builder[public reactor.core.publisher.Mono...
Cache entry for key '6189242609a72e0bacae1787' found in cache 'items'
我们可以看到几乎相同的输出。只是这一次,当在缓存中找到项目时,不会再进行额外的数据库查找。使用这种解决方案,当缓存过期时可能会出现潜在问题。由于我们使用了二级缓存,所以需要在两个缓存上设置适当的过期时间。一般原则是Flux
缓存的TTL应该比@Cacheable
更长。
4. 使用Caffeine
由于Reactor 3附加组件将在下一个版本中被弃用(从3.6.0开始)[1],我们将使用Caffeine来展示缓存的实现。在这个例子中,我们将配置Caffeine缓存:
public ItemService(ItemRepository repository) {
this.repository = repository;
this.cache = Caffeine.newBuilder().build(this::getItem_withAddons);
}
在ItemService
构造函数中,我们初始化了Caffeine缓存的基本配置,而在新的服务方法中,我们使用了这个缓存:
@Cacheable("items")
public Mono<Item> getItem_withCaffeine(String id) {
return cache.asMap().computeIfAbsent(id, k -> repository.findById(id).cast(Item.class));
}
当我们重新运行之前的测试时,我们将得到与前一个示例相似的输出。
5. 总结
在这篇文章中,我们探讨了Spring WebFlux如何与@Cacheable
交互。此外,我们描述了它们的使用方式以及一些常见问题。如往常一样,本文的代码可以从GitHub上找到。