在 Spring Webflux 中使用 @Cacheable 注解缓存结果
1、概览
本文将带你了解如何在 Spring WebFlux 中使用 @Cacheable
注解实现缓存,以及一些常见的问题和解决办法。
2、@Cacheable
和响应式类型
在本文撰稿时,@Cacheable
还不能和响应式框架无缝整合。主要问题在于,目前还没有非阻塞式的缓存实现(JSR-107 缓存 API 是阻塞式的)。只有 Redis 提供了响应式驱动。
虽然,仍然可以在方法上使用 @Cacheable
。这会缓存封装对象(Mono
或 Flux
),但不会缓存方法的实际结果。
2.1、项目设置
创建一个使用响应式 MongoDB 驱动的 Spring WebFlux 项目。并且用 Testcontainers 代替真实运行的 MongoDB 进行测试。
测试类使用 @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 传递给 Spring Boot 以自动配置 Mongo Repository。
创建带有保存和获取 Item
方法的 ItemService
类:
@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
中,为缓存和 Repository 设置 logger,这样就可以监控测试中发生的情况:
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 会检查缓存中是否有此 Item,但没有找到,因此会访问 MongoDB 来获取此记录。在第二次调用 getItem
时,Spring 会再次检查缓存并找到该 Key 的条目,但仍会访问 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'
你可以看到几乎相似的输出结果。只是这一次,当在缓存中找到一个 Item 时,不需要进行额外的数据库查找。采用这种解决方案后,缓存过期时可能会出现问题。由于使用的是缓存的缓存,因此需要在两个缓存上都设置适当的过期时间。经验法则是 Flux
缓存 TTL
应长于 @Cacheable
。
4、使用 Caffeine
由于 Reactor 3 addon 在 3.6.0 版本中弃用,因此只使用 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
交互,以及常见的一些问题和解决办法。
Ref:https://www.baeldung.com/spring-webflux-cacheable