Redis
Redis в приложении обычно появляется как «положить значение и забрать». Проблема начинается не с команд, а с соединения: оно одно, оно живёт неделями, и в резидентном воркере его нельзя дать двум запросам сразу. winter-redis закрывает именно это — соединениями управляет пул, а прикладной код видит стор с понятными методами.
Зачем это нужно
Проблема. Соединение с Redis — это один сокет с последовательным протоколом: команда → ответ → следующая команда. Пока по нему идёт один запрос, второй через него не пройдёт. В классическом PHP это не имело значения: процесс обслуживал один запрос и умирал. Резидентный воркер Winter обслуживает много запросов одновременно, каждый в своей корутине, — и одно общее соединение здесь означает, что две корутины пишут в него вперемежку. Ответ приходит на чужой запрос, в логе появляется «packets out of order», а под нагрузкой падает воркер целиком.
Открывать соединение на каждый запрос — другая крайность: рукопожатие,
аутентификация и выбор базы на каждый чих, а тысяча одновременных запросов
превращается в тысячу соединений и max number of clients reached на сервере.
Решение. Набор готовых соединений, из которого запрос берёт одно на время
работы и возвращает по завершении. Соединения переиспользуются, их число ограничено
сверху, а мёртвые заменяются — именно это делает winter-redis поверх
CPool.
Если приложение маленькое, всё это может быть не нужно
Одно соединение на запрос через #[Bean(scope: Scope::Request)] — корректный и
достаточный вариант для многих проектов, и он описан в
Базовых подключениях. Пул нужен, когда становится
заметна его цена: переоткрытие на каждом запросе и ничем не ограниченное число
соединений.
Из чего состоит
Три слоя, каждым можно пользоваться отдельно.
Конфигурация
Класс, описывающий одну точку подключения: хост, порт, пароль, номер базы, таймауты, сериализатор. Одна база — один класс конфигурации; подробно — Конфигурация.
Пул
RedisPool держит по пулу на каждый класс конфигурации. Под Swoole соединение
берётся при первом обращении в корутине и возвращается автоматически, когда она
завершилась; без Swoole — одно самоподдерживающееся соединение на процесс. Прикладной
код об этом различии не знает. Подробно — Пул соединений.
Стор
Именованный кусок базы: префикс ключей плюс те команды, которыми пользуются чаще всего. Это обычный объект — его внедряют, им пользуются, его тестируют. Подробно — Сторы, Хеши, Списки и Стримы.
Как это выглядит целиком
<?php
namespace Main\Configurations;
use Flytachi\Winter\Redis\Config\RedisConfig;
class MainRedisConfig extends RedisConfig
{
public function setUp(): void
{
$this->host = env('REDIS_HOST', 'localhost');
$this->port = (int) env('REDIS_PORT', 6379);
$this->password = env('REDIS_PASS', '');
}
}<?php
namespace Main\Stores;
use Flytachi\Winter\Redis\Store\RedisStore;
use Main\Configurations\MainRedisConfig;
class SessionStore extends RedisStore
{
protected string $redisConfigClassName = MainRedisConfig::class;
protected string $prefix = 'session:';
}use Flytachi\Winter\DI\Attribute\Autowired;
use Main\Stores\SessionStore;
class AuthService
{
#[Autowired]
private SessionStore $sessions;
public function remember(int $userId, string $token): void
{
$this->sessions->set((string) $userId, $token, ttl: 3600);
}
public function forget(int $userId): void
{
$this->sessions->delete((string) $userId);
}
}Ни получения соединения, ни его возврата, ни select() — соединение берётся при
первой команде и уходит обратно в пул, когда запрос закончился.
Установка
composer require flytachi/winter-redisНужен PHP 8.4+ и расширение ext-redis. ext-swoole опционален: с ним работает
настоящий пул, без него — одно соединение с теми же проверками живости и сроком
жизни.
Дальше
- Конфигурация — классы конфигурации, базы, сериализация, размеры пула
- Сторы — префикс, значения, сроки жизни,
raw() - Хеши — работа с полями и их сроками жизни
- Списки — очереди, потолок длины, блокирующее чтение
- Стримы — журнал событий и группы потребителей
- Пул соединений — как устроен, как настроить, что делать при переполнении