1. Введение
В этой статье мы объясним, как Spring WebFlux взаимодействует с аннотацией @Cacheable
. Во-первых, мы рассмотрим некоторые распространенные проблемы и способы их избежать. Далее мы рассмотрим доступные обходные пути. Наконец, как всегда, мы приведем примеры кода.
2. @Cacheable
и реактивные типы
Эта тема еще относительно новая. На момент написания этой статьи не было полной интеграции между @Cacheable
и реактивными фреймворками. Основная проблема заключается в отсутствии неблокирующих реализаций кеша (API кеша JSR-107 блокирует). Только 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.foreach.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.foreach.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'
Мы видим почти аналогичный результат. Только на этот раз при обнаружении элемента в кеше не требуется дополнительного поиска в базе данных. С этим решением существует потенциальная проблема, когда срок действия нашего кеша истекает. Поскольку мы используем кеш кеша, нам нужно установить соответствующие сроки действия для обоих кешей. Эмпирическое правило заключается в том, что TTL кэша Flux должен быть больше, чем
@Cacheable.
4. Использование надстройки Reactor
Надстройка Reactor 3 позволяет нам свободно использовать различные реализации кеша с классами CacheMono
и CacheFlux
. В этом примере мы настроим кеш Caffeine :
public ItemService(ItemRepository repository) {
this.repository = repository;
this.cache = Caffeine.newBuilder().build(this::getItem_withAddons);
}
В конструкторе ItemService
мы инициализируем кеш Caffeine с минимальной конфигурацией, а в новом сервисном методе используем этот кеш:
@Cacheable("items")
public Mono<Item> getItem_withAddons(String id) {
return CacheMono.lookup(cache.asMap(), id)
.onCacheMissResume(() -> repository.findById(id).cast(Object.class)).cast(Item.class);
}
Поскольку CacheMono
внутренне работает с классом Signal
, нам нужно выполнить приведение типов, чтобы вернуть соответствующие объекты.
Когда мы повторно запустим тест ранее, мы получим такой же вывод, как и в предыдущем примере.
5. Вывод
В этой статье мы рассмотрели, как Spring WebFlux взаимодействует с @Cacheable
. Кроме того, мы описали, как их можно использовать, и некоторые распространенные проблемы. Как всегда, код из этой статьи можно найти на GitHub .