HTML Diff
0 added 0 removed
Original 2026-01-01
Modified 2026-02-21
1 <p><a>#статьи</a></p>
1 <p><a>#статьи</a></p>
2 <ul><li>14 апр 2021</li>
2 <ul><li>14 апр 2021</li>
3 <li>0</li>
3 <li>0</li>
4 </ul><p>Учимся ускорять нашу реляционную базу и измерять эффект от кэширования.</p>
4 </ul><p>Учимся ускорять нашу реляционную базу и измерять эффект от кэширования.</p>
5 <p>скриншот из игры marvel vs capcom: infinite / capcom, marvel</p>
5 <p>скриншот из игры marvel vs capcom: infinite / capcom, marvel</p>
6 <p>Разрабатывает приложения на Java, воспитывает двух котов: Котлин и Монго.</p>
6 <p>Разрабатывает приложения на Java, воспитывает двух котов: Котлин и Монго.</p>
7 <p>Сегодня мы создадим простое приложение, которое взаимодействует с базой данных MySQL, и применим механизм кэширования Redis. Приложение и обе базы развернём с помощью docker-контейнеров.</p>
7 <p>Сегодня мы создадим простое приложение, которое взаимодействует с базой данных MySQL, и применим механизм кэширования Redis. Приложение и обе базы развернём с помощью docker-контейнеров.</p>
8 <p>Если вы ещё не знакомы с Redis - начните с <a>этой статьи</a>, а о работе с Docker читайте<a>здесь</a>.</p>
8 <p>Если вы ещё не знакомы с Redis - начните с <a>этой статьи</a>, а о работе с Docker читайте<a>здесь</a>.</p>
9 <p>Это будет spring-boot-приложение для хранения книг в базе данных (книжный онлайн-магазин).</p>
9 <p>Это будет spring-boot-приложение для хранения книг в базе данных (книжный онлайн-магазин).</p>
10 <p>Из-за частых запросов в базу подобные приложения работают медленно. Поэтому мы задействуем механизм кэширования - стратегию, позволяющую сохранять результаты запросов в оперативной памяти, что повысит скорость работы при повторном выполнении тех же запросов.</p>
10 <p>Из-за частых запросов в базу подобные приложения работают медленно. Поэтому мы задействуем механизм кэширования - стратегию, позволяющую сохранять результаты запросов в оперативной памяти, что повысит скорость работы при повторном выполнении тех же запросов.</p>
11 <p>Иными словами, если данные есть в кэше - берём их оттуда; иначе выполняем более тяжёлый запрос - из постоянного хранилища.</p>
11 <p>Иными словами, если данные есть в кэше - берём их оттуда; иначе выполняем более тяжёлый запрос - из постоянного хранилища.</p>
12 <ol><li>Устанавливаем Docker<a>по инструкции</a>с официального сайта.</li>
12 <ol><li>Устанавливаем Docker<a>по инструкции</a>с официального сайта.</li>
13 <li>Генерируем наш проект с помощью инструмента<a>Spring Initializr</a>. Выбираем нужные зависимости (компоненты Spring, подключаемые к проекту):</li>
13 <li>Генерируем наш проект с помощью инструмента<a>Spring Initializr</a>. Выбираем нужные зависимости (компоненты Spring, подключаемые к проекту):</li>
14 </ol><ul><li>Spring Web,</li>
14 </ol><ul><li>Spring Web,</li>
15 <li>Spring Data JPA,</li>
15 <li>Spring Data JPA,</li>
16 <li>MySQL Driver,</li>
16 <li>MySQL Driver,</li>
17 <li>Spring Data Redis</li>
17 <li>Spring Data Redis</li>
18 <li>и Lombok (по желанию).</li>
18 <li>и Lombok (по желанию).</li>
19 </ul><p>3. Скачиваем и распаковываем полученный архив, открываем его в нашей среде разработки.</p>
19 </ul><p>3. Скачиваем и распаковываем полученный архив, открываем его в нашей среде разработки.</p>
20 <p>В открывшемся проекте создаём такую структуру каталогов (готовый код<a>тут</a>):</p>
20 <p>В открывшемся проекте создаём такую структуру каталогов (готовый код<a>тут</a>):</p>
21 <p>Начнём разработку с модели, а именно с класса Book (сущность, хранимая в базе данных):</p>
21 <p>Начнём разработку с модели, а именно с класса Book (сущность, хранимая в базе данных):</p>
22 @Data @Entity public class Book implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String description; private BigDecimal price; }<p>@Data - lombok-аннотация, генерирующая шаблонный код (конструкторы, геттеры, сеттеры и так далее.</p>
22 @Data @Entity public class Book implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String description; private BigDecimal price; }<p>@Data - lombok-аннотация, генерирующая шаблонный код (конструкторы, геттеры, сеттеры и так далее.</p>
23 public interface BookRepository extends JpaRepository&lt;Book, Long&gt; { }<p><strong>BookRepository</strong> - интерфейс, расширяющий интерфейс JpaRepository. Spring Data JPA также избавляет нас от необходимости реализовывать CRUD-операции.</p>
23 public interface BookRepository extends JpaRepository&lt;Book, Long&gt; { }<p><strong>BookRepository</strong> - интерфейс, расширяющий интерфейс JpaRepository. Spring Data JPA также избавляет нас от необходимости реализовывать CRUD-операции.</p>
24 <p>Наибольший интерес представляет класс сервиса. Сервис - промежуточный слой между контроллером, обрабатывающим HTTP-запросы, и репозиторием. Каждый метод помечен аннотациями, которые обеспечивают кэширование.</p>
24 <p>Наибольший интерес представляет класс сервиса. Сервис - промежуточный слой между контроллером, обрабатывающим HTTP-запросы, и репозиторием. Каждый метод помечен аннотациями, которые обеспечивают кэширование.</p>
25 @Service @CacheConfig(cacheNames = "bc") public class BookService { private final BookRepository bookRepository; @Autowired public BookService(BookRepository bookRepository) { this.bookRepository = bookRepository; } @Cacheable public List&lt;Book&gt; findAll() { return bookRepository.findAll(); } @Cacheable(key = "#id") public Optional&lt;Book&gt; 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); } }<p>@CacheConfig - аннотация конфигурирует все кэш-операции данного класса.</p>
25 @Service @CacheConfig(cacheNames = "bc") public class BookService { private final BookRepository bookRepository; @Autowired public BookService(BookRepository bookRepository) { this.bookRepository = bookRepository; } @Cacheable public List&lt;Book&gt; findAll() { return bookRepository.findAll(); } @Cacheable(key = "#id") public Optional&lt;Book&gt; 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); } }<p>@CacheConfig - аннотация конфигурирует все кэш-операции данного класса.</p>
26 <p>@Cacheable - говорит, что результат работы метода попадает в кэш и при последующем вызове берётся оттуда (по ключу, указанному в параметре).</p>
26 <p>@Cacheable - говорит, что результат работы метода попадает в кэш и при последующем вызове берётся оттуда (по ключу, указанному в параметре).</p>
27 <p>@CachePut - позволяет обновить запись в кэше.</p>
27 <p>@CachePut - позволяет обновить запись в кэше.</p>
28 <p>@CacheEvict - удаляет запись из кэша.</p>
28 <p>@CacheEvict - удаляет запись из кэша.</p>
29 <p>Класс<strong>BookController</strong>содержит конечные точки для всех вызовов разрабатываемого API.</p>
29 <p>Класс<strong>BookController</strong>содержит конечные точки для всех вызовов разрабатываемого API.</p>
30 @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&lt;List&lt;Book&gt;&gt; findAll() { long startTime = System.currentTimeMillis(); List&lt;Book&gt; books = bookService.findAll(); long endTime = System.currentTimeMillis() - startTime; logger.info("Duration = {}", endTime); return ResponseEntity.status(HttpStatus.OK) .body(books); } @GetMapping("/{id}") public ResponseEntity&lt;Book&gt; findById(@PathVariable Long id) { return ResponseEntity.status(HttpStatus.OK) .body(bookService.findById(id).get()); } @PostMapping public ResponseEntity&lt;Book&gt; create(@RequestBody Book book) { return ResponseEntity.status(HttpStatus.CREATED) .body(bookService.save(book)); } @PutMapping("/{id}") public ResponseEntity&lt;Book&gt; 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(); } }<p>Аннотации @GetMapping, @PostMapping и @PutMapping обозначают вызов соответствующего http-метода по пути, указанному в параметре.</p>
30 @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&lt;List&lt;Book&gt;&gt; findAll() { long startTime = System.currentTimeMillis(); List&lt;Book&gt; books = bookService.findAll(); long endTime = System.currentTimeMillis() - startTime; logger.info("Duration = {}", endTime); return ResponseEntity.status(HttpStatus.OK) .body(books); } @GetMapping("/{id}") public ResponseEntity&lt;Book&gt; findById(@PathVariable Long id) { return ResponseEntity.status(HttpStatus.OK) .body(bookService.findById(id).get()); } @PostMapping public ResponseEntity&lt;Book&gt; create(@RequestBody Book book) { return ResponseEntity.status(HttpStatus.CREATED) .body(bookService.save(book)); } @PutMapping("/{id}") public ResponseEntity&lt;Book&gt; 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(); } }<p>Аннотации @GetMapping, @PostMapping и @PutMapping обозначают вызов соответствующего http-метода по пути, указанному в параметре.</p>
31 <p>Обратите внимание на операцию получения списка всех книг findAll () - мы рассчитываем время её выполнения и результат пишем в лог.</p>
31 <p>Обратите внимание на операцию получения списка всех книг findAll () - мы рассчитываем время её выполнения и результат пишем в лог.</p>
32 <p>Ещё нам нужно добавить аннотацию @EnableCaching в главный класс приложения. Это позволит запускать постпроцессор для обработки других аннотаций и обработки кэширования.</p>
32 <p>Ещё нам нужно добавить аннотацию @EnableCaching в главный класс приложения. Это позволит запускать постпроцессор для обработки других аннотаций и обработки кэширования.</p>
33 @EnableCaching @SpringBootApplication public class RedisCacheApplication { public static void main(String[] args) { SpringApplication.run(RedisCacheApplication.class, args); } }<p>Воспользуемся Docker. Нам нужно подготовить развёртывание трёх наших сервисов: MySQL, Redis и самого приложения.</p>
33 @EnableCaching @SpringBootApplication public class RedisCacheApplication { public static void main(String[] args) { SpringApplication.run(RedisCacheApplication.class, args); } }<p>Воспользуемся Docker. Нам нужно подготовить развёртывание трёх наших сервисов: MySQL, Redis и самого приложения.</p>
34 <p>Создадим Dockerfile для описания образа нашего приложения:</p>
34 <p>Создадим Dockerfile для описания образа нашего приложения:</p>
35 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"]<p>Затем создадим файл docker-compose (описывает несколько связанных между собой контейнеров):</p>
35 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"]<p>Затем создадим файл docker-compose (описывает несколько связанных между собой контейнеров):</p>
36 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"<p>В нашем случае файл содержит описание трёх сервисов с именами контейнеров, образов и обозначением портов. Параметром environment задаются переменные среды.</p>
36 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"<p>В нашем случае файл содержит описание трёх сервисов с именами контейнеров, образов и обозначением портов. Параметром environment задаются переменные среды.</p>
37 <p>Это делается в файле<strong>application.properties</strong>:</p>
37 <p>Это делается в файле<strong>application.properties</strong>:</p>
38 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<p>Обратите внимание, что в spring.datasource.url и spring.redis.host задаются хосты контейнеров.</p>
38 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<p>Обратите внимание, что в spring.datasource.url и spring.redis.host задаются хосты контейнеров.</p>
39 <p>spring.cache.redis.time-to-live задаёт время существования параметра в кэше.</p>
39 <p>spring.cache.redis.time-to-live задаёт время существования параметра в кэше.</p>
40 <p>Это делается командами:</p>
40 <p>Это делается командами:</p>
41 <p>./mvnw clean package -Dmaven.test.skip=true - собираем проект в JAR-файл.</p>
41 <p>./mvnw clean package -Dmaven.test.skip=true - собираем проект в JAR-файл.</p>
42 <p>docker-compose up - инициируем выполнение файла docker-compose.yml - а именно сборку трёх образов и создание контейнеров.</p>
42 <p>docker-compose up - инициируем выполнение файла docker-compose.yml - а именно сборку трёх образов и создание контейнеров.</p>
43 <p>Результат отразится в терминале:</p>
43 <p>Результат отразится в терминале:</p>
44 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 ---&gt; a7b99112d065 Step 2/3 : COPY /target/redis-cache-0.0.1-SNAPSHOT.jar redis-cache-0.0.1-SNAPSHOT.jar ---&gt; cd9351197743 Step 3/3 : ENTRYPOINT ["java","-jar","redis-cache-0.0.1-SNAPSHOT.jar"] ---&gt; Running in c0b7fe2de08a Removing intermediate container c0b7fe2de08a ---&gt; 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<p>Последние четыре строки означают успешные сборки.</p>
44 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 ---&gt; a7b99112d065 Step 2/3 : COPY /target/redis-cache-0.0.1-SNAPSHOT.jar redis-cache-0.0.1-SNAPSHOT.jar ---&gt; cd9351197743 Step 3/3 : ENTRYPOINT ["java","-jar","redis-cache-0.0.1-SNAPSHOT.jar"] ---&gt; Running in c0b7fe2de08a Removing intermediate container c0b7fe2de08a ---&gt; 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<p>Последние четыре строки означают успешные сборки.</p>
45 <p>Чтобы протестировать работу сервисов, несколько раз выполним post-запрос, добавляющий новую книгу:</p>
45 <p>Чтобы протестировать работу сервисов, несколько раз выполним post-запрос, добавляющий новую книгу:</p>
46 curl --location --request POST 'localhost:8080/api/v1/books' \ --header 'Content-Type: application/json' \ --data-raw '{ "name": "book1", "description": "blabla", "price": 100500 }'<p>Это можно сделать через терминал или в Postman.</p>
46 curl --location --request POST 'localhost:8080/api/v1/books' \ --header 'Content-Type: application/json' \ --data-raw '{ "name": "book1", "description": "blabla", "price": 100500 }'<p>Это можно сделать через терминал или в Postman.</p>
47 <p>Далее в отдельном терминале выполняем команды:</p>
47 <p>Далее в отдельном терминале выполняем команды:</p>
48 MBP-Maksim:~ mikheev$ docker exec -it rc-redis sh # redis-cli 127.0.0.1:6379&gt; KEYS * 1) "bc::2" 2) "bc::1" 127.0.0.1:6379&gt; FLUSHALL OK 127.0.0.1:6379&gt;<p>Здесь мы подключаемся к контейнеру Redis и проверяем, что добавленные в базу книги попали и в кэш. Для чистоты эксперимента - очищаем кэш.</p>
48 MBP-Maksim:~ mikheev$ docker exec -it rc-redis sh # redis-cli 127.0.0.1:6379&gt; KEYS * 1) "bc::2" 2) "bc::1" 127.0.0.1:6379&gt; FLUSHALL OK 127.0.0.1:6379&gt;<p>Здесь мы подключаемся к контейнеру Redis и проверяем, что добавленные в базу книги попали и в кэш. Для чистоты эксперимента - очищаем кэш.</p>
49 <p>Далее подключаемся к контейнеру основного приложения в режиме чтения логов с помощью команды:</p>
49 <p>Далее подключаемся к контейнеру основного приложения в режиме чтения логов с помощью команды:</p>
50 docker logs -f 6f767bc19768<p>Флаг -f означает чтение логов в режиме реального времени (новые логи будут последовательно выводиться в консоль в порядке их появления), а 6f767bc19768 - идентификатор контейнера (его можно получить командой docker ps).</p>
50 docker logs -f 6f767bc19768<p>Флаг -f означает чтение логов в режиме реального времени (новые логи будут последовательно выводиться в консоль в порядке их появления), а 6f767bc19768 - идентификатор контейнера (его можно получить командой docker ps).</p>
51 <p>Затем несколько раз выполняем запрос списка книг:</p>
51 <p>Затем несколько раз выполняем запрос списка книг:</p>
52 curl --location --request GET 'localhost:8080/api/v1/books'<p>и в логах получаем результат:</p>
52 curl --location --request GET 'localhost:8080/api/v1/books'<p>и в логах получаем результат:</p>
53 ^[[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<p>Видим, что первый запрос длился 676 мс (Duration) - его результаты выбирались из базы MySQL. А вот результаты последующих двух брались уже из кэша - и эти запросы выполнились в 60 раз быстрее.</p>
53 ^[[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<p>Видим, что первый запрос длился 676 мс (Duration) - его результаты выбирались из базы MySQL. А вот результаты последующих двух брались уже из кэша - и эти запросы выполнились в 60 раз быстрее.</p>
54 <p>Повторная проверка после очистки кэша подтверждает это:</p>
54 <p>Повторная проверка после очистки кэша подтверждает это:</p>
55 ^[[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<p>Вот мы и доказали эффективность Redis для кэширования данных - ускорили взаимодействие с реляционной базой.</p>
55 ^[[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<p>Вот мы и доказали эффективность Redis для кэширования данных - ускорили взаимодействие с реляционной базой.</p>
56 <a><b>Бесплатный курс по Python ➞</b>Мини-курс для новичков и для опытных кодеров. 4 крутых проекта в портфолио, живое общение со спикером. Кликните и узнайте, чему можно научиться на курсе. Смотреть программу</a>
56 <a><b>Бесплатный курс по Python ➞</b>Мини-курс для новичков и для опытных кодеров. 4 крутых проекта в портфолио, живое общение со спикером. Кликните и узнайте, чему можно научиться на курсе. Смотреть программу</a>