Статьи Генератор паролей UUID MD5 Unix-время URL-encode Base64 Форматирование XML Ваш внешний IP Число прописью


Кеширование в Spring Boot

Вернуться назад Исходники

15 февраля 2020

Тэги: Spring Boot Spring gradle Java 10 maven

Spring Boot поддерживает простой механизм кеширования данных. Рассмотрим его на примере, исходники которого доступны на github.

Создадим стандартное приложение Spring Boot. Это удобно делать через Spring Initializr (start.spring.io). В итоге, если вы используете gradle, то в файле build.gradle должны быть две зависимости:

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-cache'
    implementation 'org.springframework.boot:spring-boot-starter-web'
}

Если же вы используете maven, то зависимости будут в файле pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

Зависимость spring-boot-starter-web - это базовая функциональность нашего веб-приложения, в том числе поддержка rest-контроллеров. Зависимость spring-boot-starter-cache добавляет возможность кеширования.

Предположим, что мы хотим кешировать некие записи, представленные классом Record:

public class Record {
    private int id;
    private LocalTime creationTime;
    // конструктор и get-методы
}

Сам класс содержит всего лишь два поля: числовой идентификатор и время создания для наглядной демонстрации изменения состояния кеша.

Предположим, что извлечение этих данных без кеширования является дорогой по времени операцией. Для этого создадим сервис, который будет имитировать «тяжёлое» обращение к хранилищу с данными, которое мы хотим закешировать.

@Service
public class HighloadService {
}

В реальных проектах обращение к БД, либо к удалённому сервису может быть очень долгим, поэтому имеет смысл кешировать редко изменяющиеся объекты.

Далее добавляем в сервис три метода: getOrCreateRecord, createOrUpdateRecord и deleteRecord.

Метод getOrCreateRecord() возвращает объект по его id. Имитация «тяжёлого» обращения к БД делается с помощью метода TimeUnit.SECONDS.sleep(3), который приостанавливает выполнение потока на 3 секунды. Затем просто создаём новый объект и возвращаем его как результат. Аннотация @Cacheable говорит, что возвращаемый результат необходимо положить в кеш, если его там нет. А если он там есть, то просто вернуть значение из кеша. Параметр cacheNames указывает на имя кеша, а key указывает параметр метода, по значению которого проверять наличие элемента в кеше. Обратите внимание на символ «#», стоящий перед именем параметра.

@Cacheable(cacheNames = "recordsCache", key = "#recordId")
public Record getOrCreateRecord(int recordId) {
    try {
        TimeUnit.SECONDS.sleep(3);
        // запись будет создана в кеше только 1 раз
        return new Record(recordId, LocalTime.now());
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}

Таким образом при первом обращении к методу вы заметите задержку в 3 секунды, а затем получите в ответ созданный объект. При последующих обращениях с тем же id результат будет возвращаться мгновенно, т.к. значения будут браться из кеша и дата создания объекта меняться не будет.

Метод createOrUpdateRecord() создаёт новый объект Record и возвращает его в качестве результата. Аннотация @CachePut принудительно помещает объект, возвращаемый данным методом, в кеш. Если в кеше по данному ключу уже был объект, он будет перезаписан. Имя кеша, аналогично предыдущей аннотации, указывается в параметре cacheNames, а имя параметра метода, по значению которого следует искать запись в кеше - через параметр key.

@CachePut(cacheNames = "recordsCache", key = "#recordId")
public Record createOrUpdateRecord(int recordId) {
    // запись будет создаваться (обновляться) в кеше каждый раз
    return new Record(recordId, LocalTime.now());
}

Таким образом, каждый раз после выполнения этого метода в кеше будут обновляться записи.

Наконец, метод deleteRecord() сам ничего не делает. Однако аннотация @CacheEvict удаляет запись из кеша по указанному ключу recordId. Таким образом, после выполнения данного метода в кеше будет удаляться одна запись.

@CacheEvict(cacheNames = "recordsCache", key = "#recordId")
public void deleteRecord(int recordId) {
    // запись будет удалена из кеша
}

Теперь создадим простой RestController, чтобы можно было вызывать наши методы по http.

@RestController
public class HighloadController {

    private final HighloadService highloadService;

    public HighloadController(HighloadService highloadService) {
        this.highloadService = highloadService;
    }

    @GetMapping("/{id}")
    public Record getOrCreateRecord(@PathVariable int id) {
        return highloadService.getOrCreateRecord(id);
    }

    @PutMapping("/{id}")
    public Record createOrUpdateRecord(@PathVariable int id) {
        return highloadService.createOrUpdateRecord(id);
    }

    @DeleteMapping("/{id}")
    public String deleteRecord(@PathVariable int id) {
        highloadService.deleteRecord(id);
        return "Record deleted";
    }
}

При запуске приложения данный контроллер по умолчанию будет доступен по адресу 127.0.0.1:8080. Метод getOrCreateRecord вызывается с помощью GET-запроса по адресу 127.0.0.1:8080/{id}, где id - номер нашей записи. Аналогично метод createOrUpdateRecord вызывается PUT-запросом по тому же адресу с указанием id записи. А метод deleteRecord - DELETE-запросом.

Для активации механизма кеширования не забудьте добавить аннотацию @EnableCaching в основной класс нашего приложения, туда, где находится стандартный метод main() и аннотация @SpringBootApplication.

@EnableCaching
@SpringBootApplication
public class CachingExampleApplication {

    public static void main(String[] args) {
        SpringApplication.run(CachingExampleApplication.class, args);
    }
}

Запускаем приложение и делаем GET-запрос на 127.0.0.1:8080/1. Мы наблюдаем долгий ответ, т.е. для id = 1 объекта в кеше ещё нет. Повторные запросы выполняются мгновенно, а ответ возвращается один и тот же. Далее делаем PUT-запрос на тот же адрес. Объект будет создан и принудительно помещён в кеш. Ещё раз выполняем тот же GET-запрос и наблюдаем быстрый ответ, который вернёт нам сущность, созданную методом PUT. Теперь удалим эту запись, выполнив DELETE-запрос. И затем ещё раз GET-запрос - он опять ответит с задержкой. То есть после DELETE записи в кеше не было.

Отсюда следует несколько выводов:

  1. Кеширование записей всегда происходит по указанному id. То есть по одной записи. Spring не предлагает какого-либо api для получения сразу всех закешированных записей. Также нельзя добавить сразу несколько записей в кеш.
  2. Метод с аннотацией @Cacheable не выполняется, если по указанному id в кеше уже есть запись. Она будет извлечена из кеша и возвращена в качестве результата работы метода.
  3. Метод с аннотацией @CachePut можно вызывать любое количество раз для одного и того же id. И каждый раз объект в кеше будет обновляться. При этом важно, чтобы метод, помеченный этой аннотацией, возвращал обновлённый объект.