You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
229 lines
16 KiB
229 lines
16 KiB
1 year ago
|
# 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/
|