Go to file
Rinsvent b090cf6d25 Добавил статью про редис
Подготовил окружение для знакомства с инструментом
2023-06-12 19:55:05 +07:00
images Добавил статью про редис 2023-06-12 19:55:05 +07:00
scripts Добавил статью про редис 2023-06-12 19:55:05 +07:00
.gitignore Добавил статью про редис 2023-06-12 19:55:05 +07:00
docker-compose.yml Добавил статью про редис 2023-06-12 19:55:05 +07:00
README.md Добавил статью про редис 2023-06-12 19:55:05 +07:00

Redis guide

Redis - key / value хранилище. Обладает множеством внутренних механизмов. Хорошо подходит для кеширования, хранения временной информации, агрегации большого количества событий, отправки уведомлений подписантам (например, через веб сокет сервис).

Скачиваем урок

git clone https://git.rinsvent.ru/lessons/redis.git

Запустить контейнер

docker compose up -d

Подключаемся через PhpStorm к базе

Выбираем драйвер

img.png

Сейчас redis доступен в текущей сети без проверки прав доступа.

Переходим в терминал

img.png

Область применения

Сессии

Redis отлично подходит для хранения сессий пользователей. Он предоставляет ряд преимуществ:

  • так как это внешний сервис, то становится возможным поднять несколько инстансов приложения, тем самым увеличив пропускную способность приложения. В данном случае при перенаправлении запроса с 1 инстанса на другой, пользователь останется аутентифицированным и не получит негативный опыт работы с приложением.
  • redis не блокируется при открытии сессии, что позволяет обрабатывать несколько http запросов параллельно, что хорошо сказывается на perfomance приложения
  • redis может шардироваться, что позволяет приложению расти вместе с его аудиторией.

В разных фреймворках уже есть штатные средства и документация по настройке.

Со стороны php.ini это может выглядеть так

[Session]
session.save_handler = redis
session.save_path = "tcp://redis:6379?persistent=1&weight=1&timeout=1000"

Временные данные

Еще одним хорошим примером использования redis может быть хранение временных токенов. Например, короткие ссылки или код подтверждения который приходит в смс / email.

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

SET token:auth:rinsvent007@gmail.com "Ald4H" EX 20

После выполнения запроса в интерфейсе мы можем увидеть наше новое значение

img.png

Также мы можем получить значение командой:

GET token:auth:rinsvent007@gmail.com

Попробуйте выполнить запрос спустя 20 секунд. В ответе будет null. Также попробуйте обновить список ключей в интерфейсе PhpStorm. Ключ должен пропасть из списка.

img.png

Благодаря ключу EX в запросе мы сделали данные временными. ttl ключа в данном случае составляет 20 секунд. По истечение времени redis сам удаляет данные с диска и они станут недоступными.

Этот функционал позволяет легко реализовывать временные данные. И функционал подтверждения авторизации через код будет включать следующие шаги.

  • При попытке авторизации нам с фронта приходит логин или email или phone.
  • Дальше генерируем название ключа чтобы оно включало общий префикс - token:auth: . К префиксу добавляем данные пришедшие с формы
  • Генерируем временный токен (рандомная строка фиксированной длины).
  • Теперь сохраняем значение в сгенерированный ключ
  • Отправляем сгенерированный токен на почту или по смс на указанный номер.
  • Если пользователь является хозяином указанных email / phone, то он получает токен и вводит в форму. Отправляет токен во 2 запросе вместе с логин / email / phone
  • На основе пришедших данных мы снова генерируем название ключа в редисе и ищем значение.
  • Если оно совпадает, то авторизуем пользователя (сохраняем данные в сессию, или генерируем jwt, или любой другой способ)
  • Если значение не совпало - возвращаем ошибку

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

Redis desktop manager

Стоит уточнить что стандартным разделителем в redis является :

В интерфейсе PhpStorm ключи выводятся сплошной портянкой. В нем не очень удобно просматривать данные, искать ключи и в целом работать. Есть более удобный инструмент Установить можно через snap по ссылке https://snapcraft.io/redis-desktop-manager Нужно выполнить команду

sudo snap install redis-desktop-manager

Этот клиент работает быстрее. Поиск в нем удобнее. RDM парсит ключи и разбивает их на директории по сепаратору :. Каждая часть - отдельная директория.

Блокировки

В redis есть функционал проверки - "А был ли ключ ранее установлен?". Это достигается с помощью флага NX В случае если значения не было, оно установится, иначе новое значение не будет установлено, а в результате будет false, по которому можно принять решение, что делать дальше. Благодаря этому инструменту возможно реализовать идемпотентное апи (https://habr.com/ru/companies/yandex/articles/442762/) или гарантировать чтобы крон задача не запускалась повторно, пока не закончилась предыдущая обработка.

Разберем пример с крон задачей: Для реализации такой проверки нужно:

  • Перед началом команды сгенерировать уникальный ключ, который не будет меняться - название класса или код команды. Например - cron:import_products
  • Выполнить команду.
SET cron:import_products 1 NX
  • Реализовать проверку результата команды. В случае если получили 1, то начинаем выполнять команду, иначе прекращаем дальнейшее выполнение.
  • По окончанию выполнения команды нужно удалить ключ, чтобы при следующем старте команды она не была остановлена.
DEL cron:import_products

Попробуйте выполнить команды выше и посмотреть на результат.

Rate limiter

Иногда нужно следить за количеством запросов к определенному апи или реализовать защиту по количеству попыток авторизации. В данном случае подойдет обычный инкремент Нужно:

  • Сгенерировать ключ. Например: rate_limiter:get_products:user_id_123 - для проверки количества запросов конкретным пользователем к конкретному апи методу или rate_limiter:login:0.0.0.0 - для проверки количества попыток авторизации по ip
  • Выполняем:
INCR rate_limiter:get_products:user_id_123 
EXPIRE rate_limiter:get_products:user_id_123 60
  • Сравниваем полученное значение с лимитом из настроек приложения.
  • Если лимит не превышен, то разрешаем запрос, иначе бросаем исключение и запрещаем доступ.

PUB / SUB

С помощью данного механизма можно открыть канал, отправлять туда сообщения. Все подписчики будут получать отправленные сообщения На практике PUB/SUB можно встретить при реализации механизма websocket Для реализации системы websocket нужно:

  • Поднять websocket сервис. Можно реализовать на nodejs или go
  • Настроить чтобы websocket сервис слушал канал в redis, например - notifications
SUBSCRIBE notifications
  • Настроить отправку событий
PUBLISH notifications 'Hello world'

Для проверки функционала нужно:

  • Зайти в контейнер
docker exec -it redis sh
  • Активировать консоль redis
redis-cli
  • Подаписаться на канал
SUBSCRIBE notifications
  • Перейти в консоль редиса в интерфейсе PhpStorm из раздела database которую мы открывали в самом начале урока и начать отправлять через нее сообщения
PUBLISH notifications 'Hello world'
  • После этого можно вернуться в sh консоль внутри docker контейнера и проверить что сообщения получены.

Атомарные операции

Реализуется через LUA скрипты Все команды написанные в таком скрипте выполняются в рамках транзакции, что гарантирует точность выполнения сценария.

Приведем пример.

Допустим у нас есть система обработки сообщений из очереди. В очередь сообщения приходят извне. Мы можем только получать и обрабатывать сообщения. Очередь обрабатывается 5 воркерами. Это означает что сообщения будут обрабатываться в произвольном порядке. В каждом сообщение есть время генерации сообщения.

Задача: не обрабатывать сообщения дата генерации которых меньше чем ранее обработанное.

Алгоритм решения может быть такой:

  • Получаем сообщение из очереди
  • Берем время создания из сообщения
  • Получаем время последнего сообщения из redis
  • Сравниваем время.
  • Если в redis дата позже чем в сообщении, то прерываем обработку, иначе сохраняем в redis новое значение и обрабатываем сообщение.

В данном случае алгоритм не идеален. Так как мы в начале делаем запрос в redis и потом сравниваем время на клиенте,то пройдет какое-то время. За этот промежуток времени другой воркер уже мог получить сообщение с датой меньше текущего сообщения и начать обработку. Причем вполне может быть так, что в данной ситуации в redis запишется значение из другого воркера, которое с меньшей датой.

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

Чтобы победить эту проблему, нужно выполнять операцию чтения записи атомарно на стороне redis. Для этого будем использовать lua скрипт. Ранее он был подготовлен и добавлен в проект scripts/setifgreater.lua Давайте попробуем им воспользоваться. Выполним поочереди команды внутри контейнера:

redis-cli -n 0 --eval /scripts/setifgreater.lua custom_key 2341
redis-cli -n 0 --eval /scripts/setifgreater.lua custom_key 2340
redis-cli -n 0 --eval /scripts/setifgreater.lua custom_key 2342

После первой команды результат 1. После второй - 0. После 3 снова 1. Можно проверить значение в базе оно будет равно 2342. При выполнении 2 команды значение 2340 не было установлено. Что и требуется для нашей задачи.

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

  • Получаем сообщение из очереди
  • Берем время создания из сообщения
  • Выполняем наш скрипт на стороне redis и передаем timestamp от даты сообщения
  • Если результат 1, то выполняем сообщение, иначе - прерываем выполнение.

При такой реализации проблемы с конкурирующими запросами не будет.

Для отладки lua скрипта можно немного изменить команду

redis-cli --ldb --eval /scripts/setifgreater.lua custom_key 2342

Опция --ldb включает режим отладки. При старте команды произойдет остановка на первой строчке скрипта. Чтобы продолжить сценарий или перейти к следующей точке остановки нужно выполнить команду continue. Для повторного выполнения скрипта нужно выполнить команду restart.

В этом режиме можно продебажить скрипт и убедиться что он работает корректно. Для вывода значений из переменных доступна функция вывода в консоль

redis.debug('Hello world')

Подробнее про отладку можно почитать тут https://redis.io/docs/manual/programmability/lua-debugging/