Skip to content

Практическая работа №22

Цель работы: научиться использовать Redis-совместимое хранилище для серверного кэширования данных веб-приложения, реализовывать стратегию Cache Aside, управлять временем жизни кэша, выполнять его инвалидирование и оценивать влияние кэширования на время выполнения запросов.

Основные теоретические положения

Section titled “Основные теоретические положения”

1. Redis и Memurai

Redis — высокопроизводительное хранилище данных типа «ключ — значение», которое хранит активно используемые данные в оперативной памяти.

Одно из распространённых применений Redis — кэширование данных веб-приложений.

В данной работе используется Memurai, реализующий Redis-совместимый протокол.

Приложение подключается к нему по адресу:

127.0.0.1:6379

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

Веб-приложение
|
| Redis Protocol
v
127.0.0.1:6379
|
v
Memurai

В программном коде необходимо использовать Redis-клиент для выбранного языка.

Например:

ЯзыкКлиент
JavaScript / TypeScriptnode-redis
Pythonredis-py
PHPPredis или phpredis
Gogo-redis
C# / .NETStackExchange.Redis
JavaJedis или Lettuce

2. Кэширование

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

Предположим, веб-приложение получает сведения о товаре из базы данных:

GET /products/1
|
v
Веб-приложение
|
v
База данных
|
v
Ответ

Если получение данных занимает:

600 мс

то каждый повторный запрос будет снова выполнять эту операцию.

При использовании Redis:

GET /products/1
|
v
Веб-приложение
|
v
Redis
/ \
HIT MISS
| |
| v
| Источник данных
| |
| v
| сохранить в Redis
| |
+--------+
|
v
Ответ

Повторное получение значения из памяти обычно выполняется значительно быстрее, чем повторное обращение к более медленному источнику данных.


3. Cache HIT и Cache MISS

Если требуемые данные уже находятся в Redis, происходит:

Cache HIT

Приложение получает данные непосредственно из кэша.

Если ключ отсутствует:

Cache MISS

приложение должно получить данные из основного источника и сохранить их в Redis.


4. Стратегия Cache Aside

В данной работе необходимо использовать стратегию Cache Aside.

Алгоритм чтения:

1. Получить запрос клиента.
2. Сформировать ключ Redis.
3. Проверить наличие ключа в Redis.
4. Если данные найдены:
вернуть данные из Redis.
5. Если данные отсутствуют:
получить данные из основного источника;
сохранить их в Redis;
вернуть данные клиенту.

Например, для товара с идентификатором 15 можно использовать ключ:

cache:product:15

5. TTL

Кэш не должен хранить данные бесконечно.

TTL — Time To Live — время жизни ключа.

Например:

SET cache:product:15 "..." EX 30

означает, что значение автоматически будет удалено через:

30 секунд

Оставшееся время можно проверить командой:

TTL cache:product:15

6. Инвалидирование кэша

Предположим, в основном источнике данных товар имеет цену:

5000

Приложение помещает данные в Redis.

Затем цена изменяется:

5000 -> 6500

Если оставить старое значение в Redis, клиент продолжит получать:

5000

до истечения TTL.

Такие данные называются устаревшими.

Поэтому после изменения исходных данных соответствующий ключ необходимо удалить:

DEL cache:product:15

Следующий запрос вызовет Cache MISS, получит новое значение и снова заполнит кэш.


7. Измерение эффективности

Оптимизация должна подтверждаться измерениями.

Необходимо сравнить:

Tsource

— время получения данных из основного источника;

и:

Tcache

— время получения тех же данных из Redis.

Во сколько раз кэш оказался быстрее:

K = Tsource / Tcache

Процент сокращения времени:

P = (Tsource - Tcache) / Tsource * 100%

Задания для выполнения

Section titled “Задания для выполнения”

Для выполнения работы можно использовать любой язык программирования и веб-фреймворк.

Примеры:

JavaScript / TypeScript + Express / Fastify / NestJS
Python + Flask / FastAPI / Django
PHP + Laravel / Symfony
Go + net/http / Gin / Fiber
C# + ASP.NET Core
Java + Spring Boot

Конкретный выбор технологии на результат выполнения работы не влияет.

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

  • HTTP-маршруты;
  • Redis-ключи;
  • алгоритмы;
  • TTL;
  • правила инвалидирования;
  • критерии проверки.

Задание 1. Проверка работы Memurai

Откройте терминал Windows:

PowerShell

или:

Command Prompt

Выполните:

Terminal window
memurai-cli PING

Ожидаемый ответ:

PONG

Создайте тестовый ключ:

Terminal window
memurai-cli SET lab:test "Redis works"

Получите его значение:

Terminal window
memurai-cli GET lab:test

Ожидаемый результат:

Redis works

Удалите ключ:

Terminal window
memurai-cli DEL lab:test

Повторите:

Terminal window
memurai-cli GET lab:test

После удаления значение должно отсутствовать.


Создайте ключ со временем жизни 30 секунд:

Terminal window
memurai-cli SET lab:ttl "temporary value" EX 30

Получите оставшееся время:

Terminal window
memurai-cli TTL lab:ttl

Повторите команду через несколько секунд:

Terminal window
memurai-cli TTL lab:ttl

Запишите результаты:

Первое значение TTL: __________
Второе значение TTL: __________
Третье значение TTL: __________

После истечения времени выполните:

Terminal window
memurai-cli GET lab:ttl

Объясните полученный результат.


Задание 2. Создание исходного веб-приложения

Создайте небольшое веб-приложение на выбранном языке программирования.

Приложение должно содержать следующие товары:

[
{
"id": 1,
"name": "Mechanical Keyboard",
"price": 8500
},
{
"id": 2,
"name": "Wireless Mouse",
"price": 4200
},
{
"id": 3,
"name": "USB-C Hub",
"price": 5600
}
]

Способ хранения исходных данных выберите самостоятельно.

Допускается использовать:

массив;
список;
словарь;
Map;
коллекцию объектов;
JSON-файл;
SQLite;
другую локальную СУБД.

Для данной работы использование отдельной СУБД не является обязательным.

Реализуйте функцию или метод:

getProductById(id)

который возвращает товар по идентификатору.

Перед возвратом результата добавьте искусственную задержку:

600–800 мс

Например, логика функции должна соответствовать следующему псевдокоду:

function getProductById(id):
wait 700 milliseconds
find product by id
return product

Задержка имитирует:

  • запрос к СУБД;
  • обращение к внешнему API;
  • сложное вычисление;
  • другую дорогостоящую операцию.

Реализуйте маршрут:

GET /products/:id

Например:

GET /products/1

Ответ должен иметь вид:

{
"source": "database",
"durationMs": 703,
"data": {
"id": 1,
"name": "Mechanical Keyboard",
"price": 8500
}
}

Поле:

durationMs

должно содержать время обработки запроса внутри приложения.

Если товар не существует, сервер должен вернуть HTTP-код:

404 Not Found

Задание 3. Измерение работы приложения без кэширования

Последовательно выполните запрос:

GET /products/1

не менее 5 раз.

Запишите результаты.

№ запросаИсточникВремя выполнения, мс
1database
2database
3database
4database
5database

Рассчитайте среднее время:

Tsource = __________________ мс

Ответьте на вопрос:

Почему время выполнения повторных запросов практически не уменьшается?


Задание 4. Подключение приложения к Memurai

Установите Redis-клиент для выбранного языка программирования.

Приложение должно подключаться к:

Host: 127.0.0.1
Port: 6379

или использовать URI:

redis://127.0.0.1:6379

После подключения выполните из программного кода команду:

PING

Убедитесь, что сервер возвращает:

PONG

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


Задание 5. Реализация Cache Aside

Измените обработку маршрута:

GET /products/:id

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

cache:product:{id}

Например:

cache:product:1
cache:product:2
cache:product:3

Реализуйте следующий алгоритм:

получить id товара
|
v
сформировать ключ
cache:product:{id}
|
v
получить значение из Redis
|
+---------- значение найдено ----------+
| |
| v
| десериализовать
| |
| v
| Cache HIT
| |
| v
| вернуть ответ
|
|
+---------- значение отсутствует
|
v
получить исходные данные
|
v
сериализовать данные
|
v
сохранить в Redis
с TTL 30 секунд
|
v
Cache MISS
|
v
вернуть ответ

Для хранения объекта в Redis сериализуйте его в формат:

JSON

TTL должен составлять:

30 секунд

При получении данных из основного источника API должен возвращать:

{
"source": "database",
"durationMs": 704,
"data": {
"id": 1,
"name": "Mechanical Keyboard",
"price": 8500
}
}

При получении данных из Redis:

{
"source": "cache",
"durationMs": 3,
"data": {
"id": 1,
"name": "Mechanical Keyboard",
"price": 8500
}
}

Дополнительно добавьте HTTP-заголовок:

при Cache HIT:

X-Cache: HIT

при Cache MISS:

X-Cache: MISS

Задание 6. Проверка Cache HIT и Cache MISS

Перед началом проверки удалите возможный ключ:

Terminal window
memurai-cli DEL cache:product:1

Выполните:

GET /products/1

Первый запрос должен вернуть:

source: database
X-Cache: MISS

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

Теперь должно быть:

source: cache
X-Cache: HIT

Выполните запрос ещё несколько раз.

Зафиксируйте результаты:

№X-CacheSourceВремя, мс
1MISSdatabase
2HITcache
3HITcache
4HITcache
5HITcache

Объясните, почему первый запрос выполняется значительно дольше последующих.


Задание 7. Исследование содержимого кэша

После выполнения:

GET /products/1

проверьте наличие ключа:

Terminal window
memurai-cli EXISTS cache:product:1

Получите его содержимое:

Terminal window
memurai-cli GET cache:product:1

Пример результата:

{"id":1,"name":"Mechanical Keyboard","price":8500}

Проверьте оставшееся время:

Terminal window
memurai-cli TTL cache:product:1

Подождите несколько секунд и снова выполните:

Terminal window
memurai-cli TTL cache:product:1

После истечения 30 секунд снова выполните:

Terminal window
memurai-cli GET cache:product:1

Затем:

GET /products/1

Запишите:

Источник после истечения TTL: __________________
Время выполнения: __________________ мс

Объясните, почему после истечения TTL запрос снова выполняется медленно.


Задание 8. Реализация инвалидирования кэша

Добавьте возможность изменения товара:

PUT /products/:id

Например:

PUT /products/1
Content-Type: application/json

Тело запроса:

{
"name": "Mechanical Keyboard PRO",
"price": 10000
}

После изменения товара необходимо удалить соответствующий ключ Redis:

DEL cache:product:{id}

Алгоритм:

PUT /products/1
|
v
изменить исходные данные
|
v
DEL cache:product:1
|
v
вернуть обновлённый товар

Проверьте работу по следующему сценарию.

Шаг 1.

Выполните:

GET /products/1

Убедитесь, что данные появились в кэше.


Шаг 2.

Проверьте:

Terminal window
memurai-cli GET cache:product:1

Шаг 3.

Измените цену товара:

8500 -> 10000

через:

PUT /products/1

Шаг 4.

Проверьте:

Terminal window
memurai-cli GET cache:product:1

Ключ должен отсутствовать.


Шаг 5.

Снова выполните:

GET /products/1

Запрос должен вернуть:

X-Cache: MISS
source: database

и новую цену:

10000

Шаг 6.

Сразу повторите:

GET /products/1

Теперь ожидается:

X-Cache: HIT
source: cache

Объясните, почему после изменения товара кэш необходимо удалить.


Задание 9. Сравнение производительности

Удалите кэш:

Terminal window
memurai-cli DEL cache:product:1

Выполните первый запрос:

GET /products/1

Зафиксируйте его время:

Cache MISS: __________ мс

После этого выполните не менее 5 запросов с Cache HIT.

№РезультатВремя, мс
1MISS
2HIT
3HIT
4HIT
5HIT
6HIT

Рассчитайте среднее время запросов, выполненных через кэш:

Tcache = __________________ мс

Используйте полученное ранее:

Tsource = __________________ мс

Рассчитайте:

K = Tsource / Tcache

Получите:

Кэширование ускорило получение данных примерно в __________ раз.

Дополнительно рассчитайте:

P = (Tsource - Tcache) / Tsource * 100%
Время выполнения уменьшилось примерно на __________ %.

Сделайте вывод, дала ли выполненная оптимизация измеримый эффект.


Задание 10. Самостоятельное расширение кэширования

Реализуйте маршрут:

GET /products

который возвращает список всех товаров.

При первом запросе данные должны поступать из основного источника.

Для кэширования списка используйте ключ:

cache:products:all

Установите TTL:

20 секунд

Первый запрос:

X-Cache: MISS

Последующие:

X-Cache: HIT

При выполнении:

PUT /products/:id

необходимо удалить:

cache:product:{id}

и:

cache:products:all

То есть после изменения товара логика инвалидирования должна соответствовать:

DEL cache:product:{id}
DEL cache:products:all

Проверьте следующий сценарий:

1. GET /products
2. GET /products
3. PUT /products/2
4. GET /products
5. GET /products

Ожидаемая последовательность:

MISS
HIT
изменение данных
MISS
HIT

Объясните, почему при изменении одного товара необходимо удалить не только:

cache:product:2

но и:

cache:products:all

Требования к результату

Section titled “Требования к результату”

К концу работы приложение должно поддерживать:

GET /products/:id
PUT /products/:id
GET /products

В приложении должны быть реализованы:

подключение к Memurai;
получение данных из Redis;
сохранение данных в Redis;
JSON-сериализация и десериализация;
Cache HIT;
Cache MISS;
Cache Aside;
TTL;
инвалидирование кэша;
заголовок X-Cache;
измерение времени выполнения запросов.

Структура ключей должна иметь следующий вид:

cache:product:1
cache:product:2
cache:product:3
cache:products:all

Что необходимо продемонстрировать при защите

Section titled “Что необходимо продемонстрировать при защите”

Продемонстрируйте преподавателю:

1. Первый запрос к товару — Cache MISS.
2. Повторный запрос — Cache HIT.
3. Наличие значения через memurai-cli.
4. Работу TTL.
5. Автоматическое исчезновение ключа после истечения TTL.
6. Инвалидирование после PUT-запроса.
7. Новый Cache MISS после изменения данных.
8. Последующий Cache HIT.
9. Результаты измерения времени до и после оптимизации.

Будьте готовы изменить TTL, название Redis-ключа или добавить новый кэшируемый маршрут по требованию преподавателя.


  1. Что такое Redis?
  2. Для каких задач в веб-приложениях может использоваться Redis?
  3. Что называется кэшем?
  4. Что означает Cache HIT?
  5. Что означает Cache MISS?
  6. В чём заключается стратегия Cache Aside?
  7. Почему данные сначала проверяются в Redis, а только затем запрашиваются из основного источника?
  8. Что такое TTL?
  9. Что произойдёт с ключом после истечения TTL?
  10. Как узнать оставшееся время жизни Redis-ключа?
  11. Для чего при сохранении объекта используется сериализация?
  12. Почему слишком маленький TTL может снизить эффективность кэширования?
  13. Чем опасен слишком большой TTL?
  14. Что такое инвалидирование кэша?
  15. Почему после изменения данных недостаточно изменить только основной источник?
  16. Почему после изменения одного товара необходимо инвалидировать кэш этого товара?
  17. Почему при кэшировании списка товаров необходимо дополнительно удалить кэш списка?
  18. Почему первый запрос обычно выполняется медленнее повторного?
  19. Можно ли считать уменьшение времени запроса оптимизацией, если оно не было измерено до и после изменения приложения?
  20. В каких ситуациях использование Redis для кэширования может практически не давать выигрыша?

Рекомендуемая литература

Section titled “Рекомендуемая литература”
  1. Redis Documentation — Cache Aside.
  2. Redis Documentation — Redis client libraries.
  3. Redis Documentation — команда GET.
  4. Redis Documentation — команда SET.
  5. Redis Documentation — команда DEL.
  6. Redis Documentation — команда TTL.
  7. Redis Documentation — команда EXPIRE.
  8. Memurai Documentation — Installing Memurai for Windows.
  9. Memurai Documentation — Memurai Command Line Interface.