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