引言

本文将解释Spring WebFlux如何与@Cacheable注解交互。首先,我们将讨论一些常见问题以及如何避免它们。接下来,我们会介绍可用的解决方法。最后,如往常一样,我们提供代码示例。

2. @Cacheable与反应式类型

这个主题相对较新。撰写本文时,@Cacheable与反应式框架之间还没有流畅的集成。主要问题是,没有非阻塞缓存实现(JSR-107缓存API是阻塞的)。只有Redis提供了反应式驱动器。

尽管我们在上一段提到的问题,我们仍然可以在服务方法上使用@Cacheable。这会导致我们的包装对象(MonoFlux)被缓存,但不会缓存方法的实际结果。

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类,包含savegetItem方法:

@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的结果

MonoFlux具有内置的缓存机制,我们可以在这种情况下使用它作为工作绕过。如前所述,@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上找到。