commit
b090cf6d25
@ -0,0 +1,2 @@ |
|||||||
|
db-data |
||||||
|
.idea |
@ -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/ |
@ -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 |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 27 KiB |
@ -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