Добавил статью про редис

Подготовил окружение для знакомства с инструментом
This commit is contained in:
Rinsvent 2023-06-12 19:55:05 +07:00
commit b090cf6d25
8 changed files with 264 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
db-data
.idea

229
README.md Normal file
View File

@ -0,0 +1,229 @@
# 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/

18
docker-compose.yml Normal file
View File

@ -0,0 +1,18 @@
version: '3.8'
services:
redis:
image: redis:7.0.9
container_name: redis
restart: always
ports:
- "6379:6379"
volumes:
- ./scripts:/scripts
networks:
- general
networks:
general:
external:
name: general

BIN
images/draiver.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
images/refresh.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
images/terminal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
images/token.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

15
scripts/setifgreater.lua Normal file
View File

@ -0,0 +1,15 @@
local value = tonumber(redis.call('GET', KEYS[1]))
if value == nil then
value = 0
end
redis.debug('asdfasdf')
local newValue = tonumber(KEYS[2])
if newValue > value then
redis.call('SET', KEYS[1], newValue)
return 1;
end
return 0;