Вступление

Привет! Вы когда-нибудь задумывались, почему некоторые запросы в микросервисах ощущаются как поездка на «старой электричке»? Казалось бы, есть FeignClient — мощный и удобный инструмент для общения сервисов, но внезапно задержки растут, а коллеги начинают замечать, что ваше API «тормозит».

Я расскажу, как я решил эту проблему, добавив кэширование с помощью Caffeine Cache. После этого мой сервис стал выдавать данные быстрее, чем их запрашивали (шутка, но почти правда).

Готовы? Тогда поехали.

Стек технологий

Для реализации использовались следующие инструменты:

  • Java 21 — на текущий момент это последняя стабильная версия языка;

  • Spring Boot 3.1 — актуальная версия с поддержкой Jakarta EE и обновлённой экосистемой;

  • Spring Cloud OpenFeign — 2023.x.x, одна из последних версий с поддержкой современных фич;

  • Caffeine Cache 3.x — высокопроизводительный инструмент кэширования;

  • Gradle — для сборки проекта.

Кэширование с FeignClient можно внедрить начиная с:

  • Java 8 — базовая версия для использования Caffeine Cache

  • Spring Boot 2.1+ — поддержка интеграции FeignClient через Spring Cloud

  • Caffeine Cache 2.x+ — основные функции кэширования

Однако для максимальной производительности рекомендую использовать Spring Boot 3.x и Java 17 или выше.

Что такое FeignClient и зачем ему кэш?

FeignClient — это такая магия в мире Spring, которая позволяет взаимодействовать с другими сервисами так, будто вы вызываете локальный метод. Никакой головной боли с HTTP-запросами, сериализацией и прочими мелочами.

Но у любого чуда есть обратная сторона: каждое обращение к FeignClient — это сетевой вызов. А там:

  • Латентность сети (время, которое требуется на доставку пакета данных от источника к пункту назначения);

  • Нагрузки на удалённые сервисы;

  • Зависимость от стабильности другого API

Если ваш сервис часто запрашивает одни и те же данные, то вопрос «А нельзя ли это как-то ускорить?» становится очевидным. Здесь на сцену выходит Caffeine Cache.

Как настроить кэширование для FeignClient

Первый шаг: добавляем зависимости.

Если ваш проект ещё не знает, что такое Feign и Caffeine, научите его, для этого необходимо добавить в ваш build.gradle следующие зависимости:

dependencies {
    implementation group: 'org.springframework.boot', name: 'spring-boot-starter-cache', version: '3.3.5'
    implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-openfeign', version: '4.1.3'
    implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'

    testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: '3.3.5'
}

Теперь ваш проект готов к магии.

Второй шаг: создаём FeignClient

Как говорится, в начале было слово... а точнее, интерфейс. Вот так будет выглядеть наш клиент:

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(name = "exampleClient", url = "https://api.example.com")
public interface ExampleFeignClient {
    @GetMapping("/data")
    ExampleResponse getData(@RequestParam String id);
}

Это простой клиент, который обращается к внешнему API. Пока он делает запросы напрямую.

Ответ от клиента: создаём ExampleResponse

Наш FeignClient возвращает объект ExampleResponse. Определим его класс:

public class ExampleResponse {
    private String data;

    public ExampleResponse(String data) {
        this.data = data;
    }

    public String getData() {
        return data;
    }

    public void setData(String data) {
        this.data = data;
    }
}

Третий шаг: включаем кэширование и настраиваем Caffeine Cache

Caffeine — это Ferrari в мире кэширования. Лёгкий, быстрый, гибкий. Давайте добавим его в наш проект и включим поддержку кэширования в Spring Boot, добавив аннотацию @EnableCaching:

import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import java.time.Duration;

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    @Primary
    public CacheManager cacheManager() {
      var caffeineCacheManager = new CaffeineCacheManager("exampleCache");
      caffeineCacheManager.setCaffeine(
            Caffeine.newBuilder()
                    .expireAfterWrite(Duration.ofMinutes(10)) // Данные устаревают через 10 минут
                    .maximumSize(100) // Максимум 100 записей                    
      );
      return caffeineCacheManager;       
    }
  
}

Теперь Spring Cache знает, как работать с Caffeine. У нас есть кэш, который будет хранить до 100 записей в течение 10 минут.

Зачем нужна аннотация @Primary?

Аннотация @Primary позволяет Spring однозначно выбрать, какой бин использовать по умолчанию, когда тип CacheManager запрашивается в вашем приложении. Если вы добавляете несколько кэш-менеджеров (например, для разных технологий или целей), Spring может не знать, какой из них предпочтителен.

Четвёртый шаг: соединяем FeignClient и кэширование при помощи аннотации @Cacheable

Вот где начинается магия. Вместо того чтобы вызывать FeignClient напрямую, мы добавляем слой кэширования. Используем аннотацию @Cacheable в сервисе, чтобы автоматически кэшировать результаты вызовов FeignClient:

import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class ExampleService {
    private final ExampleFeignClient feignClient;

    public ExampleService(ExampleFeignClient feignClient) {
        this.feignClient = feignClient;
    }

    @Cacheable(value = "exampleCache", cacheManager = "cacheManager", key = "#id")
    public ExampleResponse getDataWithCaching(String id) {
        System.out.println("Запрос через FeignClient для ID: " + id);
        return feignClient.getData(id);
    }
}

Теперь каждый вызов метода getDataWithCaching сначала проверяет, есть ли данные в кэше. Если они есть — возвращает результат из кэша. Если нет — вызывает FeignClient и кэширует результат.

Проверяем, работает ли

Если вы, как и я, любите убедиться, что код работает идеально, давайте напишем тест:

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;

@SpringBootTest
class ExampleServiceTest {
    @Autowired
    private ExampleService exampleService;

    @MockBean
    private ExampleFeignClient feignClient;

    @Test
    void testCaching() {
        String id = "123";
        ExampleResponse response = new ExampleResponse("Test Data");

        // Имитируем ответ FeignClient
        Mockito.when(feignClient.getData(id)).thenReturn(response);

        // Первый вызов - данные будут запрошены через FeignClient
        ExampleResponse result1 = exampleService.getDataWithCaching(id);
        Assertions.assertEquals(response, result1);

        // Второй вызов - данные должны быть взяты из кэша
        ExampleResponse result2 = exampleService.getDataWithCaching(id);
        Assertions.assertEquals(response, result2);

        // Проверяем, что FeignClient вызван только один раз
        Mockito.verify(feignClient, Mockito.times(1)).getData(id);
    }
}

Тест гарантирует, что кэш действительно работает.

Финальное слово

Добавив кэширование к FeignClient, вы не просто ускоряете свой сервис — вы создаёте более устойчивую и надёжную архитектуру, что особенно важно, когда ваша система работает под высокой нагрузкой.

Кэширование — это не просто про производительность. Это про оптимизацию ресурсов, стабильность и удовольствие от быстродействия.

Использование @Cacheable значительно упрощает добавление кэширования в проект.
Теперь ваш FeignClient стал быстрее, а пользователи довольны скоростью отклика.

Если вы ещё не используете кэширование в микросервисах — сейчас самое время начать! Если у вас есть вопросы или вы хотите поделиться своим опытом, пишите в комментариях.

Кэширование: далеко за пределами FeignClient

Кэширование в Spring — это универсальный инструмент, который может быть использован в самых разных сценариях. FeignClient — лишь один из примеров, где кэширование помогает ускорить выполнение запросов. Но его потенциал гораздо шире:

  1. Кэширование данных из базы данных
    Например, если ваш сервис часто обращается к базе за данными, которые редко меняются, кэширование запросов к репозиториям может значительно снизить нагрузку:

@Cacheable(value = "userCache", key = "#userId")  
public User getUserById(String userId) {  
    return userRepository.findById(userId).orElseThrow();  
}  
  1. Результаты сложных вычислений
    Если вы выполняете ресурсоёмкие вычисления (например, обработки больших объёмов данных), кэширование может стать спасением:

@Cacheable(value = "calculationCache", key = "#input")  
public CalculationResult calculateHeavyTask(InputData input) {  
    // Затратная операция  
}  
  1. Взаимодействие с внешними API
    Помимо FeignClient, кэширование отлично подходит для любого взаимодействия с внешними сервисами, чтобы минимизировать сетевую нагрузку.

  2. Часто используемые настройки или метаданные
    Если ваш сервис часто запрашивает статические данные (например, конфигурации, справочники или метаданные), кэширование ускоряет доступ:

@Cacheable(value = "configCache")  
public Config getConfig() {  
    return loadConfigFromFileOrDatabase();  
}  
  1. Работа с асинхронными потоками
    Для асинхронных операций кэширование также может быть настроено через асинхронный бин Caffeine.

Вывод

Кэширование — это универсальный инструмент, который работает везде, где требуется оптимизация доступа к данным. Главное — правильно определить, какие данные действительно стоит кэшировать, и выбрать подходящую стратегию.

Комментарии (4)


  1. shipovalov
    21.11.2024 03:28

    Спасибо за статью.

    Из названия думал будет кэширование в FeignClient-е, а в статье кэшируется вызов метода в сервисе


  1. sirojiddin13
    21.11.2024 03:28

    Лучше все же вытащить кеш во вне, в Redis тот же, что бы избежать OOM приложения, ну и что бы кеш не зависимо от рантайма приложения (при перезапуске Caffeine не сохранит кеш), ну и плюс если у вас будут реплики приложений, то с Caffeine у каждого приложения будет свой кеш, а это большее потребление памяти, проблемы с рассинхронизацией кеша


    1. nickesss
      21.11.2024 03:28

      Ну вы конечно писсимистически рассматриваете подход. Суть кеша же не выгрузить гиг данных из БД и сложить их в кеш (ну чтобы получить ООМ), так что вы это зря. Ну и то что редис лежит рядом это прекрасно но это не отменяет летанси запросов к нему. Caffein и Redis предназначены для немного разных вещей. Можно же использовать их оба.


  1. PavlushaCh
    21.11.2024 03:28

    В целом автора поддерживаю - кэширование инструмент очень полезный и порой незаменимый.

    Единственное не понял при чем тут FeignClient)

    Тут описано старое доброе кэширование результатов методов спринговых бинов) И кстати ничего нет про инвалидацию кэша. Кэшировать результат вызова метода - это одно. Но как узнать, что на сервере что-то поменялось и данные в кэше стали не валидны. Как правило если нет ответа на этот вопрос включать кэширование методов будет самоубийством.

    Да и на практике никогда не встречал микросервисы в котором результаты метода на 100% предсказуемы и никогда не меняются.

    Спасибо автору за пост.