Перейти к основному содержимому

Spring Webflux и аннотация @Cacheable

· 5 мин. чтения

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 .