commit b090cf6d25c7d7dc62de07c37d37502dbeb95eb0 Author: Rinsvent Date: Mon Jun 12 19:55:05 2023 +0700 Добавил статью про редис Подготовил окружение для знакомства с инструментом diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..678b837 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +db-data +.idea \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6d721d3 --- /dev/null +++ b/README.md @@ -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/ \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1101a03 --- /dev/null +++ b/docker-compose.yml @@ -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 diff --git a/images/draiver.png b/images/draiver.png new file mode 100644 index 0000000..010676e Binary files /dev/null and b/images/draiver.png differ diff --git a/images/refresh.png b/images/refresh.png new file mode 100644 index 0000000..9809824 Binary files /dev/null and b/images/refresh.png differ diff --git a/images/terminal.png b/images/terminal.png new file mode 100644 index 0000000..acd9496 Binary files /dev/null and b/images/terminal.png differ diff --git a/images/token.png b/images/token.png new file mode 100644 index 0000000..df932bf Binary files /dev/null and b/images/token.png differ diff --git a/scripts/setifgreater.lua b/scripts/setifgreater.lua new file mode 100644 index 0000000..055a70e --- /dev/null +++ b/scripts/setifgreater.lua @@ -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; +