Redis для кэширования. Ускоряем взаимодействие с основной базой
2026-02-21 01:13 Diff

#статьи

  • 14 апр 2021
  • 0

Учимся ускорять нашу реляционную базу и измерять эффект от кэширования.

скриншот из игры marvel vs capcom: infinite / capcom, marvel

Разрабатывает приложения на Java, воспитывает двух котов: Котлин и Монго.

Сегодня мы создадим простое приложение, которое взаимодействует с базой данных MySQL, и применим механизм кэширования Redis. Приложение и обе базы развернём с помощью docker-контейнеров.

Если вы ещё не знакомы с Redis — начните с этой статьи, а о работе с Docker читайте здесь.

Это будет spring-boot-приложение для хранения книг в базе данных (книжный онлайн-магазин).

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

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

  1. Устанавливаем Docker по инструкции с официального сайта.
  2. Генерируем наш проект с помощью инструмента Spring Initializr. Выбираем нужные зависимости (компоненты Spring, подключаемые к проекту):
  • Spring Web,
  • Spring Data JPA,
  • MySQL Driver,
  • Spring Data Redis
  • и Lombok (по желанию).

3. Скачиваем и распаковываем полученный архив, открываем его в нашей среде разработки.

В открывшемся проекте создаём такую структуру каталогов (готовый код тут):

Начнём разработку с модели, а именно с класса Book (сущность, хранимая в базе данных):

@Data @Entity public class Book implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String description; private BigDecimal price; }

@Data — lombok-аннотация, генерирующая шаблонный код (конструкторы, геттеры, сеттеры и так далее.

public interface BookRepository extends JpaRepository<Book, Long> { }

BookRepository — интерфейс, расширяющий интерфейс JpaRepository. Spring Data JPA также избавляет нас от необходимости реализовывать CRUD-операции.

Наибольший интерес представляет класс сервиса. Сервис — промежуточный слой между контроллером, обрабатывающим HTTP-запросы, и репозиторием. Каждый метод помечен аннотациями, которые обеспечивают кэширование.

@Service @CacheConfig(cacheNames = "bc") public class BookService { private final BookRepository bookRepository; @Autowired public BookService(BookRepository bookRepository) { this.bookRepository = bookRepository; } @Cacheable public List<Book> findAll() { return bookRepository.findAll(); } @Cacheable(key = "#id") public Optional<Book> findById(Long id) { return bookRepository.findById(id); } @CachePut(key = "#book.id") public Book save(Book book) { return bookRepository.save(book); } @CacheEvict(key = "#id") public void deleteById(Long id) { bookRepository.deleteById(id); } }

@CacheConfig — аннотация конфигурирует все кэш-операции данного класса.

@Cacheable — говорит, что результат работы метода попадает в кэш и при последующем вызове берётся оттуда (по ключу, указанному в параметре).

@CachePut — позволяет обновить запись в кэше.

@CacheEvict — удаляет запись из кэша.

Класс BookController содержит конечные точки для всех вызовов разрабатываемого API.

@RestController @RequestMapping("/api/v1/books") public class BookController { private final static Logger logger = LoggerFactory.getLogger(BookController.class); private final BookService bookService; @Autowired public BookController(BookService bookService) { this.bookService = bookService; } @GetMapping public ResponseEntity<List<Book>> findAll() { long startTime = System.currentTimeMillis(); List<Book> books = bookService.findAll(); long endTime = System.currentTimeMillis() - startTime; logger.info("Duration = {}", endTime); return ResponseEntity.status(HttpStatus.OK) .body(books); } @GetMapping("/{id}") public ResponseEntity<Book> findById(@PathVariable Long id) { return ResponseEntity.status(HttpStatus.OK) .body(bookService.findById(id).get()); } @PostMapping public ResponseEntity<Book> create(@RequestBody Book book) { return ResponseEntity.status(HttpStatus.CREATED) .body(bookService.save(book)); } @PutMapping("/{id}") public ResponseEntity<Book> update(@PathVariable Long id, @RequestBody Book book) { return ResponseEntity.status(HttpStatus.ACCEPTED) .body(bookService.save(book)); } public ResponseEntity delete(@PathVariable Long id) { bookService.deleteById(id); return ResponseEntity.status(HttpStatus.ACCEPTED).build(); } }

Аннотации @GetMapping, @PostMapping и @PutMapping обозначают вызов соответствующего http-метода по пути, указанному в параметре.

Обратите внимание на операцию получения списка всех книг findAll () — мы рассчитываем время её выполнения и результат пишем в лог.

Ещё нам нужно добавить аннотацию @EnableCaching в главный класс приложения. Это позволит запускать постпроцессор для обработки других аннотаций и обработки кэширования.

@EnableCaching @SpringBootApplication public class RedisCacheApplication { public static void main(String[] args) { SpringApplication.run(RedisCacheApplication.class, args); } }

Воспользуемся Docker. Нам нужно подготовить развёртывание трёх наших сервисов: MySQL, Redis и самого приложения.

Создадим Dockerfile для описания образа нашего приложения:

FROM adoptopenjdk/openjdk11:alpine-jre COPY /target/redis-cache-0.0.1-SNAPSHOT.jar redis-cache-0.0.1-SNAPSHOT.jar ENTRYPOINT ["java","-jar","redis-cache-0.0.1-SNAPSHOT.jar"]

Затем создадим файл docker-compose (описывает несколько связанных между собой контейнеров):

version: '3' services: rc-mysql: container_name: rc-mysql image: mysql/mysql-server:5.7 environment: MYSQL_DATABASE: rc MYSQL_ROOT_PASSWORD: root MYSQL_ROOT_HOST: '%' ports: - "3306:3306" restart: always rc-redis: container_name: rc-redis image: redis:5 ports: - "6379:6379" restart: always redis-cache: build: ./ ports: - "8080:8080" depends_on: - "rc-mysql" - "rc-redis"

В нашем случае файл содержит описание трёх сервисов с именами контейнеров, образов и обозначением портов. Параметром environment задаются переменные среды.

Это делается в файле application.properties:

spring.datasource.url=jdbc:mysql://rc-mysql:3306/rc?useSSL=false spring.datasource.username=root spring.datasource.password=root spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.jpa.hibernate.ddl-auto=create spring.jpa.database-platform=org.hibernate.dialect.MySQL57Dialect spring.jpa.generate-ddl=true spring.jpa.show-sql=true spring.redis.host=rc-redis spring.redis.timeout=2000 spring.cache.redis.time-to-live=100000 spring.data.redis.repositories.enabled=false

Обратите внимание, что в spring.datasource.url и spring.redis.host задаются хосты контейнеров.

spring.cache.redis.time-to-live задаёт время существования параметра в кэше.

Это делается командами:

./mvnw clean package -Dmaven.test.skip=true — собираем проект в JAR-файл.

docker-compose up — инициируем выполнение файла docker-compose.yml — а именно сборку трёх образов и создание контейнеров.

Результат отразится в терминале:

MBP-Maksim:redis-cache mikheev$ docker-compose up Creating network "redis-cache_default" with the default driver Building redis-cache Step 1/3 : FROM adoptopenjdk/openjdk11:alpine-jre ---> a7b99112d065 Step 2/3 : COPY /target/redis-cache-0.0.1-SNAPSHOT.jar redis-cache-0.0.1-SNAPSHOT.jar ---> cd9351197743 Step 3/3 : ENTRYPOINT ["java","-jar","redis-cache-0.0.1-SNAPSHOT.jar"] ---> Running in c0b7fe2de08a Removing intermediate container c0b7fe2de08a ---> d74e79bfc5eb Successfully built d74e79bfc5eb Successfully tagged redis-cache_redis-cache:latest Creating rc-mysql ... done Creating rc-redis ... done Creating redis-cache_redis-cache_1 ... done Attaching to rc-mysql, rc-redis, redis-cache_redis-cache_1

Последние четыре строки означают успешные сборки.

Чтобы протестировать работу сервисов, несколько раз выполним post-запрос, добавляющий новую книгу:

curl --location --request POST 'localhost:8080/api/v1/books' \ --header 'Content-Type: application/json' \ --data-raw '{ "name": "book1", "description": "blabla", "price": 100500 }'

Это можно сделать через терминал или в Postman.

Далее в отдельном терминале выполняем команды:

MBP-Maksim:~ mikheev$ docker exec -it rc-redis sh # redis-cli 127.0.0.1:6379> KEYS * 1) "bc::2" 2) "bc::1" 127.0.0.1:6379> FLUSHALL OK 127.0.0.1:6379>

Здесь мы подключаемся к контейнеру Redis и проверяем, что добавленные в базу книги попали и в кэш. Для чистоты эксперимента — очищаем кэш.

Далее подключаемся к контейнеру основного приложения в режиме чтения логов с помощью команды:

docker logs -f 6f767bc19768

Флаг -f означает чтение логов в режиме реального времени (новые логи будут последовательно выводиться в консоль в порядке их появления), а 6f767bc19768 — идентификатор контейнера (его можно получить командой docker ps).

Затем несколько раз выполняем запрос списка книг:

curl --location --request GET 'localhost:8080/api/v1/books'

и в логах получаем результат:

^[[1;2DHibernate: select book0_.id as id1_0_, book0_.description as descript2_0_, book0_.name as name3_0_, book0_.price as price4_0_ from book book0_ 2020-10-02 19:45:51.666 INFO 1 --- [nio-8080-exec-6] c.m.r.controller.BookController : Duration = 676 Hibernate: select book0_.id as id1_0_, book0_.description as descript2_0_, book0_.name as name3_0_, book0_.price as price4_0_ from book book0_ 2020-10-02 19:46:00.329 INFO 1 --- [nio-8080-exec-2] c.m.r.controller.BookController : Duration = 12 Hibernate: select book0_.id as id1_0_, book0_.description as descript2_0_, book0_.name as name3_0_, book0_.price as price4_0_ from book book0_ 2020-10-02 19:46:02.238 INFO 1 --- [nio-8080-exec-3] c.m.r.controller.BookController : Duration = 10

Видим, что первый запрос длился 676 мс (Duration) — его результаты выбирались из базы MySQL. А вот результаты последующих двух брались уже из кэша — и эти запросы выполнились в 60 раз быстрее.

Повторная проверка после очистки кэша подтверждает это:

^[[1;Hibernate: select book0_.id as id1_0_, book0_.description as descript2_0_, book0_.name as name3_0_, book0_.price as price4_0_ from book book0_ 2020-10-02 19:49:30.350 INFO 1 --- [nio-8080-exec-3] c.m.r.controller.BookController : Duration = 96 Hibernate: select book0_.id as id1_0_, book0_.description as descript2_0_, book0_.name as name3_0_, book0_.price as price4_0_ from book book0_ 2020-10-02 19:49:34.582 INFO 1 --- [io-8080-exec-10] c.m.r.controller.BookController : Duration = 6 Hibernate: select book0_.id as id1_0_, book0_.description as descript2_0_, book0_.name as name3_0_, book0_.price as price4_0_ from book book0_ 2020-10-02 19:49:54.696 INFO 1 --- [nio-8080-exec-1] c.m.r.controller.BookController : Duration = 14

Вот мы и доказали эффективность Redis для кэширования данных — ускорили взаимодействие с реляционной базой.

Бесплатный курс по Python ➞
Мини-курс для новичков и для опытных кодеров. 4 крутых проекта в портфолио, живое общение со спикером. Кликните и узнайте, чему можно научиться на курсе. Смотреть программу