# Redis guide Redis - key / value хранилище. Обладает множеством внутренних механизмов. Хорошо подходит для кеширования, хранения временной информации, агрегации большого количества событий, отправки уведомлений подписантам (например, через веб сокет сервис). ## Скачиваем урок ```bash git clone https://git.rinsvent.ru/lessons/redis.git ``` ## Запустить контейнер ```bash docker compose up -d ``` ## Подключаемся через PhpStorm к базе ### Выбираем драйвер ![img.png](images/draiver.png) Сейчас redis доступен в текущей сети без проверки прав доступа. ### Переходим в терминал ![img.png](images/terminal.png) ## Область применения ### Сессии Redis отлично подходит для хранения сессий пользователей. Он предоставляет ряд преимуществ: - так как это внешний сервис, то становится возможным поднять несколько инстансов приложения, тем самым увеличив пропускную способность приложения. В данном случае при перенаправлении запроса с 1 инстанса на другой, пользователь останется аутентифицированным и не получит негативный опыт работы с приложением. - redis не блокируется при открытии сессии, что позволяет обрабатывать несколько http запросов параллельно, что хорошо сказывается на perfomance приложения - redis может шардироваться, что позволяет приложению расти вместе с его аудиторией. В разных фреймворках уже есть штатные средства и документация по настройке. Со стороны php.ini это может выглядеть так ```ini [Session] session.save_handler = redis session.save_path = "tcp://redis:6379?persistent=1&weight=1&timeout=1000" ``` ### Временные данные Еще одним хорошим примером использования redis может быть хранение временных токенов. Например, короткие ссылки или код подтверждения который приходит в смс / email. Давайте перейдем в консоль и выполним запрос: ```redis SET token:auth:rinsvent007@gmail.com "Ald4H" EX 20 ``` После выполнения запроса в интерфейсе мы можем увидеть наше новое значение ![img.png](images/token.png) Также мы можем получить значение командой: ```redis GET token:auth:rinsvent007@gmail.com ``` Попробуйте выполнить запрос спустя 20 секунд. В ответе будет null. Также попробуйте обновить список ключей в интерфейсе PhpStorm. Ключ должен пропасть из списка. ![img.png](images/refresh.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 Нужно выполнить команду ```bash sudo snap install redis-desktop-manager ``` Этот клиент работает быстрее. Поиск в нем удобнее. RDM парсит ключи и разбивает их на директории по сепаратору `:`. Каждая часть - отдельная директория. ## Блокировки В redis есть функционал проверки - "А был ли ключ ранее установлен?". Это достигается с помощью флага NX В случае если значения не было, оно установится, иначе новое значение не будет установлено, а в результате будет false, по которому можно принять решение, что делать дальше. Благодаря этому инструменту возможно реализовать идемпотентное апи (https://habr.com/ru/companies/yandex/articles/442762/) или гарантировать чтобы крон задача не запускалась повторно, пока не закончилась предыдущая обработка. Разберем пример с крон задачей: Для реализации такой проверки нужно: - Перед началом команды сгенерировать уникальный ключ, который не будет меняться - название класса или код команды. Например - cron:import_products - Выполнить команду. ```redis SET cron:import_products 1 NX ``` - Реализовать проверку результата команды. В случае если получили 1, то начинаем выполнять команду, иначе прекращаем дальнейшее выполнение. - По окончанию выполнения команды нужно удалить ключ, чтобы при следующем старте команды она не была остановлена. ```redis DEL cron:import_products ``` Попробуйте выполнить команды выше и посмотреть на результат. ## Rate limiter Иногда нужно следить за количеством запросов к определенному апи или реализовать защиту по количеству попыток авторизации. В данном случае подойдет обычный инкремент Нужно: - Сгенерировать ключ. Например: rate_limiter:get_products:user_id_123 - для проверки количества запросов конкретным пользователем к конкретному апи методу или rate_limiter:login:0.0.0.0 - для проверки количества попыток авторизации по ip - Выполняем: ```redis 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 ```redis SUBSCRIBE notifications ``` - Настроить отправку событий ```redis PUBLISH notifications 'Hello world' ``` Для проверки функционала нужно: - Зайти в контейнер ```bash docker exec -it redis sh ``` - Активировать консоль redis ```bash redis-cli ``` - Подаписаться на канал ```redis SUBSCRIBE notifications ``` - Перейти в консоль редиса в интерфейсе PhpStorm из раздела database которую мы открывали в самом начале урока и начать отправлять через нее сообщения ```redis 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/