Добавил статью про редис
Подготовил окружение для знакомства с инструментом
This commit is contained in:
commit
b090cf6d25
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
db-data
|
||||
.idea
|
229
README.md
Normal file
229
README.md
Normal 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
18
docker-compose.yml
Normal 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
BIN
images/draiver.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
BIN
images/refresh.png
Normal file
BIN
images/refresh.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
images/terminal.png
Normal file
BIN
images/terminal.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
BIN
images/token.png
Normal file
BIN
images/token.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
15
scripts/setifgreater.lua
Normal file
15
scripts/setifgreater.lua
Normal 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;
|
||||
|
Loading…
Reference in New Issue
Block a user