Redis на практических примерах / Хабр

Redis на практических примерах / Хабр сад и огород
redis , программирование, архитектура, очереди, php, кейсы, кэширование, кэширование данных, блокировки, mutex, rate limiter, pub/sub, manychat, manychat team

Redis на практических примерах

Redis — это довольно популярный инструмент, который изначально поддерживает большое количество различных типов данных и методов работы с ними. Многие проекты используют его как слой кэша, но его функциональность гораздо шире. Мы в ManyChat очень любим Redis и активно используем его в своих продуктах для решения огромного количества задач. Вот несколько интересных примеров использования этой in-memory Key-Value базы данных. на примерах Redis — очень полезная база данных. Надеемся, что вы найдете их полезными и сможете использовать в своих проектах. применить Кое-что в вашем проекте.

Рассмотрим следующие варианты использования

  • Кэш данных (да, банально и скучно, но это полезный инструмент для кэширования, и кажется неправильным избегать этого примера)
  • Базовые операции с очередью redis
  • Организация блокировки (мьютексы)
  • Создание системы ограничения скорости
  • Pubsub — создание отправки клиенту

Redis на практических примерах / Хабр

Скрытое хранение данных

Начнем с самого простого: одним из наиболее распространенных вариантов использования Redis является кэширование данных. Это полезно для тех, кто никогда раньше не использовал Redis, так как с его помощью можно создать кэш данных, который затем можно использовать для других целей. Долгосрочные пользователи инструмента могут смело переходить к следующему примеру. Кэширование используется для снижения нагрузки на базу данных и обеспечения максимально быстрого получения часто используемых данных. Это также хранилище ключевых значений, и сложность доступа к данным на основе ключей составляет O(1). Поэтому данные могут быть получены очень быстро.

Чтобы получить данные из хранилища, можно сделать следующее

Public function getValueFromCache(string $key)< return $this->getRedis()->rawCommand('GET', $key); > 

Однако, чтобы получить данные из кэша, их нужно сначала поместить в кэш. Простой пример записи:

Public function setValueToCache(string $key, $value)< $this->getRedis()->rawCommand('SET', $key, $value); > 

Таким образом, данные можно записывать в Redis, а затем считывать обратно, используя тот же ключ, когда это необходимо. Однако по мере того, как вы продолжаете записывать данные в Redis, объем оперативной памяти, занимаемый данными в Redis, будет расти и расти. Ненужные данные необходимо удалять, что очень сложно проверять вручную, поэтому мы позволяем вам делать это автоматически. redis Позаботьтесь об этом сами — давайте добавим TTL (срок действия ключа) к ключу.

Public function setValueToCache(string $key, $value, int $ttl = 3600)< $this->getRedis()->rawCommand('SET', $key, $value, 'EX', $ttl); >

По истечении времени ttl (в секундах) данные для этого ключа будут автоматически удалены.

Как часто говорят, две самые сложные вещи в программировании — это создание имен переменных и аннулирование кэша. Чтобы принудительно удалить значения из Redis на основе каждого ключа, выполните следующую команду

Public function dropValueFromCache(string $key)< $this->getRedis()->rawCommand('DEL', $key); >

В Redis вы также можете получить массив значений через список ключей.

Public function getValuesFromCache(array $keys)< return $this->getRedis()->rawCommand('MGET', . $keys); >

Массовое удаление данных на основе таблицы ключей соответственно.

Public function dropValuesFromCache(array $keys)< $this->getRedis()->rawCommand('MDEL', . $keys); >

Ожидающие столбцы

Используя структуры данных, доступные в Redis, можно легко реализовать стандартные очереди FIFO или LIFO. Для этого используйте структуру List и методы для работы с ней. Работа с очередью состоит из двух основных действий: отправка задания в очередь и получение задания из очереди. Задания могут быть отправлены в очередь из любой точки системы. Получение и обработка заданий из очереди обычно выполняется специальным процессом, называемым потребителем.

Поэтому, чтобы отправить задание в очередь, достаточно использовать следующие методы

Public function PushToQueue(string $queueName, $payload)< $this->getRedis()->rawCommand('RPUSH', $queueName, serialize($payload)); >

Итак, добавьте определенный $payload в конец листа с именем $queueName. Это может быть JSON для инициализации необходимой бизнес-логики (например, данные для денежной транзакции, данные для инициализации отправки электронного письма пользователю и т. д.). .) Если в хранилище нет листа с именем $queueName, лист создается автоматически и в него помещается первый элемент $payload.

На стороне потребителя необходимо убедиться, что задание получено из очереди. Это делается с помощью простой команды чтения из листа; для реализации очереди FIFO используется чтение с обратной стороны (в данном случае запись через RPUSH). Другими словами, чтение через LPOP.

Public function PopFromQueue(string $queueName)< return $this->getRedis()->rawCommand('LPOP', $queueName); >

Для реализации LIFO-очереди лист должен быть прочитан с той же стороны, с которой он был записан, то есть через RPOP.

Redis на практических примерах / Хабр

Таким образом, из очереди считывается по одному сообщению за раз. Если лист не существует (пустой), возвращается NULL. Контекст потребителя выглядит следующим образом

Класс consumer< private string $queueName; public function __construct(string $queueName) < $this->queueName = $queueName; >Публичная функция run()< while (true) < //Вычитываем в бесконечном цикле нашу очередь $payload = $this->PopFromQueue(); if ($payload === null)< //Если мы получили NULL, значит очередь пустая, сделаем небольшую паузу в ожидании новых сообщений sleep(1); continue; >//Если очередь не пустая и мы получили $payload, то запускаем обработку этого $payload $this->process($payload); > >Private function popFromQueue()< return $this->getRedis()->rawCommand('LPOP', $this->queueName); > >

Чтобы получить информацию о глубине очереди (количестве значений, хранящихся в листе), можно использовать следующие команды

Public function getQueueLength(string $queueName)< return $this->getRedis()->rawCommand('LLEN', $queueName); >

Хотя в этом разделе описана базовая реализация простой очереди, Redis позволяет создавать более сложные очереди. Например, допустим, вы хотите знать, когда пользователь в последний раз выполнял какое-либо действие на вашем сайте. Вам не нужно знать с точностью до секунды. Погрешность составляет три минуты. Вы можете обновлять поле last_visit пользователя для каждого запроса от пользователя к бэкенду. Но если таких пользователей много — 10, 000 или 100, 000? А если у вас есть SPA, который отправляет большое количество асинхронных запросов? Обновление полей в базе данных для каждого такого запроса приведет к большому количеству глупых запросов в базу данных. Эту проблему можно решить несколькими способами. Один из вариантов — создать своего рода ленивую очередь, которая отбивает одну и ту же задачу по очереди. Здесь может пригодиться такая структура, как Sorted SET. Это взвешенное множество, где каждый элемент имеет свой вес (оценку). А что, если в качестве оценки использовать временную метку, когда элемент добавляется в этот отсортированный набор? Далее организуйте очередь, в которой некоторые события могут быть отложены на определенный период времени. Для этого используйте следующую функцию

Public function PushToDelayedQueue(string $queueName, $payload, int $lay = 180)< $this->getRedis()->rawCommand('ZADD', $queueName, 'NX', time() + $delay, serialize($payload)) >

В этой схеме идентификатор пользователя, посетившего сайт, ставится в очередь в $queueName и остается там в течение 180 секунд. Все остальные запросы за это время также отправляются в эту очередь, но идентификатор этого пользователя туда не добавляется, так как он уже существует в этой очереди и не повторяется (в этом участвует параметр ‘NX’). Таким образом, снижается лишняя нагрузка, и каждый пользователь не генерирует несколько запросов каждые три минуты для обновления поля last_visit.

Теперь возникает вопрос о том, как считывается эта очередь. Если методы LPOP и RPOP листа считывают значение и удаляют его из листа по отдельности (что означает, что несколько потребителей не могут получить одно и то же значение), то отсортированный набор не имеет такого метода. Считывать и удалять элементы можно только двумя последовательными командами. Однако эти команды можно выполнять отдельно с помощью простых LUA-скриптов.

Public function PopFromDelayedQueue(string $queueName).< $command = 'eval " local val = redis . call(\'ZRANGEBYSCORE\', KEYS[1], 0, ARGV[1], \'LIMIT\', 0, 1)[1] if val then redis .call(\'ZREM\', KEYS[1], val) end return val" '; return $this->getRedis()->rawCommand($command, 1, $queueName, time()); >

В этом сценарии LUA команда ZrangeByscore используется для того, чтобы попытаться получить первое значение в текущей временной шкале переменной VAL с весом от 0 до 0. Если это значение удается получить, то команда ZREM используется для удаления себя из набора сортировки и возврата себя к значению val. Все эти функции выполняются отдельно. Таким образом, можно читать как хвосты конкумера, так и хвосты, построенные поверх списка. с примером Хвосты, построенные на списках списков.

Мы описали некоторые базовые паттерны хвостов, реализованные в нашей системе. На данный момент в производстве существуют более сложные механизмы ожидания хвостов — линейные, сложные и затененные хвосты. С Redis вы можете делать все это без сложного планирования и с помощью умного и готового охлаждения.

Мьютекс (блокировка).

Мьютекс — это механизм синхронизации доступа нескольких процессов к общему ресурсу, гарантирующий, что только один процесс взаимодействует с ресурсом каждый раз. Этот механизм часто используется в биллинговых и других системах, где важна безопасность потоков.

Для реализации Mutex на базе Redis идеально подходит стандартный набор с дополнительными параметрами.

Public function lock (String $ key, string $ hash, int $ ttl = 10): bool< return (bool)$this->getRedis()->rawCommand('SET', $key, $hash, 'NX', 'EX', $ttl); >

Параметры настройки мьютекса следующие

  • $ key — ключ, идентифицирующий мьютекс,
  • $ hash — создает подпись, идентифицирующую того, кто установил мьютекс. Вы не хотите, чтобы другие случайно удалили блокировку, поэтому вся логика сводится на нет.
  • $ TTL — время действия блокировки (например, если что-то пойдет не так, блокировка по какой-то причине умрет, процесс не будет удален, чтобы блокировка не застряла на неопределенный срок).

В большинстве случаев, когда вы создаете код, который будет работать с общим ресурсом, который заблокирован, вы хотите подождать, пока блокировка не будет снята, и продолжить использование этого ресурса. Для этого можно реализовать простой способ ожидания освобождения ресурса.

Public function TryLock (String $ key, string $ hash, int $ timeout, int $ ttl = 10): bool< $startTime = microtime(true); while (!this->lock ($ key, $ hash, $ ttl).< if ((microtime(true) - $startTime) >($ timeout).< return false; // не удалось взять shared ресурс под блокировку за указанный $timeout >usleep(500 * 1000) //ждем 500 миллисекунд до следующей попытки поставить блокировку > return true; //блокировка успешно поставлена >

Теперь, когда вы поняли, как накладывается блокировка, вам нужно узнать, как ее снять. Чтобы убедиться, что блокировка снята с той процедуры, в которой она была установлена, перед удалением цены из Save Redis нужно проверить хэш, хранящийся для этого ключа. Чтобы сделать это отдельно, воспользуйтесь сценарием LUA.

Публичная функция release lock (String $ key, string $ hash): bool< $command = 'eval " if redis . call ("get", keys [1]) == возврат из argv [1]. redis .call("DEL",KEYS[1]) else return 0 end" '; return (bool) $this->getRedis()->rawCommand($command, 1, $key, $hash); >

Здесь команда get используется для того, чтобы попытаться найти цену ключа $. Если она равна $hash, то он удаляется с помощью команды Del, которая возвращает количество удаленных ключей. Она возвращает 0, потому что не равна $hash. Это означает, что блокировка не может быть снята. Основы. пример Мьютекс:

Заряды класса.< public function charge(int $userId, int $amount) < $mutexName = sprintf('billing_%d', $userId); $hash = sha1(sprintf('billing_%d_%d'), $userId, mt_rand()); //генерим некий хэш запущенного потока if (!$this->tryLock ($ MutexName, $ Hash, 10)< //пытаемся поставить блокировку в течение 10 секунд throw new Exception('Не получилось поставить lock, shared ресурс занят'); >//lock получен, процессим бизнес-логику $this->doSomeLogick(); //освобождаем shared ресурс, снимаем блокировку $this->releaseLock($mutexName, $hash); > >

Процент.

Очень часто приходится ограничивать количество обращений к API. Например, для конечной точки API от аккаунта не должно поступать более 100 запросов в минуту. Эта задача легко решается с помощью вашего любимого Redis.

Public function islimitreached (string $ method, int $ userid, int $ limit: bool< $currentTime = time(); $timeWindow = $currentTime - ($currentTime % 60); //Так как наш rate limit имеет ограничение 100 запросов в минуту, //то округляем текущий timestamp до начала минуты — это будет частью нашего ключа, //по которому мы будем считать количество запросов $key = sprintf('api_%s_%d_%d', $method, $userId, $timeWindow); //генерируем ключ для счетчика, соответственно каждую минуту он будет меняться исходя из $timeWindow $count = $this->getRedis()->rawCommand('INCR', $key); //метод INCR увеличивает значение по указанному ключу, и возвращает новое значение. //Если ключа не существует, он будут инициализирован со значением 0 и после этого увеличен $this->getRedis()->rawCommand('EXPIRE', $key, 60); // Обновляем TTL нашему ключу, выставляя его в минуту, для того, чтобы не накапливать не актуальные данные if ($count >$ lime)< //limit достигнут return true; >return false; > 

Этот простой метод позволяет ограничить количество обращений к API. Базовый каркас контроллера выглядит следующим образом

Класс Foocontroller.< public function actionBar() < if ($this->isLimitReached(__METHOD__, $this->getuserid (), 100))< throw new Exception('API method max limit reached'); >$this->doSomeLogick(); > >

Pub/Sub.

Pub/Sub — это интересный механизм, который позволяет, с одной стороны, подписаться на канал и получать сообщения, а с другой — отправлять сообщения на этот канал, приобретенные всеми подписчиками. Вероятно, многие люди, работавшие с WebSockets, соразмеряли этот механизм, но выглядит он действительно хорошо. Механизм Pub/Sub не гарантирует доставку сообщений и не гарантирует согласованность, поэтому не используйте его в системах, где эти критерии важны. Механизмы PUB/суб не следует использовать в системах, где эти критерии важны. Тем не менее, рассмотрим этот механизм на практическом примере . Предположим, у вас есть большое количество команд, содержащих дьяволов, которыми вы хотите управлять централизованно. Когда вы инициализируете команду, она регистрируется на канале, по которому принимаются командные сообщения. С другой стороны, у вас есть сценарий управления, в котором командные сообщения отправляются в указанный канал. К сожалению, стандартный PHP работает в одном заблокированном потоке. Чтобы реализовать то, что нам нужно, мы используем ReactPHP и клиент Redis.

Подпишитесь на канал:

класс foodmemon< private $throttleParam = 10; public function run() < $loop = React\EventLoop\Factory::create(); //инициализируем event-loop ReactPHP $redisClient = $this->getRedis($loop); //инициализируем клиента Redis для ReactPHP $redisClient->subscribe (__ class__);// подписываемся на нужные каналы в Redis, примере название канала соответствует названию класса $redisClient->on ( 'message', static function ($ channel, $ payload))< //слушаем события message, при возникновении такого события, получаем channel и payload switch (true) < // Здесь может быть любая логика обработки сообщений, в качестве примера пускай будет так: case \is_int($payload): //Если к нам пришло число – обновим параметр $throttleParam на полученное значение $this->throttleParam = $payload; break; case $payload === 'exit': //Если к нам пришла команда 'exit' – завершим выполнение скрипта exit; default: //Если пришло что-то другое, то просто залогируем это $this->log($payload); break; > >); $loop->addperiodictimer (0, function())< $this->doSomeLogick(); // Здесь в бесконечном цикле может выполняться какая-то логика, например чтение задач из очереди и их процессинг >); $loop->run(); //Запускаем наш event-loop > >

Отправка сообщения в канал — более простое действие. Его можно выполнить абсолютно из любой точки системы с помощью команды

Публичная функция publishmessage ($ канал, $ сообщение)< $this->getRedis()->publish($channel, $message); >

В результате отправки этого сообщения в канал все клиенты, зарегистрированные в этом канале, получат это сообщение.

Redis на практических примерах / Хабр

Результат.

Мы рассмотрели 5 примеров Мы надеемся, что каждый найдет для себя что-то интересное в использовании Redis на практике — Redis занимает важное место в нашем технологическом стеке. Мы любим этот инструмент за его скорость и гибкость. Мы используем Redis в производстве уже много лет, и он показал себя как очень хороший и надежный инструмент, поддерживающий многие части наших продуктов. Наш небольшой кластер серверов Redis обрабатывает около миллиона приложений в секунду. А как вы используете Redis в своих проектах? Поделитесь своим опытом в комментариях!

  • Блог Manychat
  • Php
  • Программирование
  • Системный анализ и проектирование
  • NOSQL &lt; PAN&gt; В результате отправки сообщения в канал, все клиенты, зарегистрированные в этом канале, получат это сообщение.
Оцените статью