Конфигурация
Конфигурация описывает одну точку подключения: куда идти, чем аутентифицироваться, какую базу выбрать. Из неё пул строит все свои соединения, поэтому всё, что должно быть одинаковым у каждого соединения, живёт здесь.
Класс конфигурации
Наследуется от RedisConfig, заполняется в setUp():
<?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', '');
}
}setUp() вызывается один раз на каждый создаваемый экземпляр — то есть на каждое
соединение в пуле. Читать здесь окружение нормально; ходить в базу или в сеть — нет.
Что можно задать
| Свойство | По умолчанию | Что это |
|---|---|---|
$host |
'localhost' |
адрес сервера; допускает схему (tls://, unix://) |
$port |
6379 |
порт |
$username |
'' |
пользователь ACL (Redis 6+); пусто — пользователь default |
$password |
'' |
пароль; пустая строка — AUTH не отправляется |
$databaseIndex |
0 |
номер базы (SELECT) |
$timeout |
1.5 |
секунд на открытие сокета |
$readTimeout |
2.0 |
секунд на ответ уже открытого соединения |
$serializer |
Redis::SERIALIZER_NONE |
как кодировать значения |
$context |
[] |
контекст потока: CA, клиентский сертификат, проверка узла |
Два таймаута, и они про разное
Их путают чаще прочего, а последствия у них разные.
$timeout — сколько ждать установления соединения. Исчерпан — соединение не
открылось, и пул получит PoolException, который наружу выйдет как
RedisPoolException. Это про недоступный сервер.
$readTimeout — сколько ждать ответа на уже отправленную команду. Исчерпан —
RedisException: read error on connection, и это выглядит как обрыв, а не как таймаут.
Это про сервер, который принял команду и молчит.
$this->timeout = 1.5; // не достучались — падаем быстро
$this->readTimeout = 2.0; // ответа нет дольше двух секунд — считаем связь потеряннойreadTimeout и блокирующие команды
BLPOP, BRPOP, SUBSCRIBE заставляют сервер молчать намеренно — ровно до
появления данных. Для соединения это неотличимо от зависшего сервера, поэтому
consume(timeout: 5) на соединении с readTimeout = 2.0 умрёт на второй секунде с
ошибкой чтения. Измерено.
Ручка списка это учитывает и поднимает читающий таймаут на время ожидания сама — но
если пишете блокирующую команду через raw(), поднимайте его руками:
$redis->setOption(Redis::OPT_READ_TIMEOUT, -1) снимает ограничение (именно -1; 0
означает «не ждать вовсе»).
Оба значения — на одно соединение, а не на запрос: пул может ждать свободного
соединения дольше, и это отдельная настройка $poolWaitTimeout.
Аутентификация
Пароля достаточно для обычной установки — он аутентифицирует пользователя default:
$this->password = env('REDIS_PASS', '');Начиная с Redis 6 есть ACL: отдельные пользователи со своими правами и доступом к
своему набору ключей. Управляемые Redis у облачных провайдеров часто выдают именно
такого пользователя, а не пароль от default:
$this->username = env('REDIS_USER', 'app');
$this->password = env('REDIS_PASS');Если $username пуст, отправляется AUTH <пароль>; если задан — AUTH <пользователь> <пароль>. Проверить, кем вы подключились, можно так:
$config->connection()->rawCommand('ACL', 'WHOAMI'); // 'app'Пароль не попадает в логи пакета
getDsn() намеренно собирается из хоста, порта и базы — ни пароля, ни пользователя в
нём нет, поэтому строку можно писать в лог целиком. Пул логирует именно её.
TLS и unix-сокет
Схема указывается прямо в адресе — драйвер разбирает её сам:
$this->host = 'tls://redis.example.com';
$this->port = 6380;Материалы для TLS — доверенный корневой сертификат, клиентский сертификат, требования к
проверке узла — передаются через $context:
$this->host = 'tls://redis.example.com';
$this->port = 6380;
$this->context = [
'stream' => [
'cafile' => '/etc/ssl/certs/redis-ca.pem',
'verify_peer' => true,
],
];Для локального сокета:
$this->host = '/var/run/redis/redis.sock';
$this->port = 0; // порт не используетсяОшибки TLS приходят предупреждениями, а не исключениями
Драйвер сообщает о неудачном рукопожатии PHP-warning’ом (Failed to enable crypto), и
соединение при этом может считаться установленным. Проверяйте настройку явным
ping() при первом деплое, а не по отсутствию исключений.
Несколько конфигураций
Конфигурация — это не «настройки Redis в приложении», а одна точка подключения. Их бывает несколько, и это нормально:
class MainRedisConfig extends RedisConfig { /* база 0 — сессии, кеш */ }
class QueueRedisConfig extends RedisConfig { /* база 1 — очереди */ }
class MetricsRedisConfig extends RedisConfig { /* другой сервер целиком */ }Каждая получает собственный пул со своим потолком, и в
RedisPool::stats() они видны по отдельности. Сторы привязываются к нужной через
$redisConfigClassName.
Разделять стоит тогда, когда у частей разные требования: очередь с блокирующими читателями не должна конкурировать за соединения с горячим кешем, а метрики не должны уронить приложение, если их сервер недоступен.
Номер базы
Задаётся в конфигурации и только там:
class QueueRedisConfig extends RedisConfig
{
public function setUp(): void
{
$this->host = env('REDIS_HOST', 'localhost');
$this->databaseIndex = 3;
}
}Нужна вторая база — второй класс конфигурации. Он получит собственный пул со своими соединениями, и путаницы между ними не возникнет по построению.
Не переключайте базу на лету
SELECT меняет состояние соединения, а соединение возвращается в пул. Следующий
запрос получил бы его вместе с чужой базой и писал бы не туда — молча, без единой
ошибки. Поэтому в API стора нет ничего похожего на select(): смена базы возможна
только вместе со сменой конфигурации.
Выбранная база переживает и обрыв связи: ext-redis переподключается прозрачно и
восстанавливает AUTH и SELECT сам, так что соединение не «просыпается» на базе
номер ноль.
Сериализация значений
По умолчанию — SERIALIZER_NONE: на сервер уходят ровно те байты, что вы передали.
Это единственная форма, которую прочитают redis-cli и приложения на других языках.
Платой за это становится то, что значение должно быть строкой или числом:
$store->set('user', ['id' => 1]); // → в базе строка "Array", и только PHP-warning
$store->get('user'); // 'Array'Ошибка не выбрасывается — поэтому выбор сериализатора и сделан свойством конфигурации, где он виден один раз и на месте:
use Redis;
class CacheRedisConfig extends RedisConfig
{
public function setUp(): void
{
$this->host = env('REDIS_HOST', 'localhost');
$this->serializer = Redis::SERIALIZER_PHP; // массивы и объекты как есть
}
}| Значение | Что даёт | Чем платите |
|---|---|---|
SERIALIZER_NONE |
байты как есть, читаемо всеми | только строки и числа |
SERIALIZER_PHP |
любые значения PHP, включая объекты | формат читает только PHP |
SERIALIZER_JSON |
любые массивы, читаемо другими языками | объекты возвращаются как stdClass |
Сериализатор меняют до того, как в базе появились данные
Он применяется к соединению, а не к ключу, поэтому уже записанные значения после смены читаться перестанут: старые байты будут разбираться новым способом. На заполненной базе это миграция, а не настройка.
Размер пула
По умолчанию пул на конфигурацию — 10 соединений, ожидание свободного — 3 секунды. Чтобы задать своё, конфигурация объявляет себя пул-осведомлённой:
<?php
namespace Main\Configurations;
use Flytachi\Winter\Redis\Config\RedisConfig;
use Flytachi\Winter\Redis\Pool\RedisPoolConfigInterface;
use Flytachi\Winter\Redis\Pool\RedisPoolTrait;
class MainRedisConfig extends RedisConfig implements RedisPoolConfigInterface
{
use RedisPoolTrait;
public int $poolMaxConnections = 20;
public float $poolWaitTimeout = 5.0;
public function setUp(): void
{
$this->host = env('REDIS_HOST', 'localhost');
}
}Трейт даёт все методы интерфейса со значениями по умолчанию, поэтому объявлять нужно только то, что меняете. Разбор каждой настройки — в Пуле соединений.
Разовое подключение
Для скрипта, миграции или теста класс заводить незачем — есть inline-вариант:
<?php
require 'vendor/autoload.php';
use Flytachi\Winter\Redis\Config\Call\RedisCall;
$redis = (new RedisCall(
host: '127.0.0.1',
port: 6379,
databaseIndex: 3,
))->connection();
$redis->set('warm', '1');Каждый такой объект открывает своё соединение и закрывает его, когда его соберёт сборщик мусора. В приложении, обслуживающем запросы, так делать не нужно — там конфигурация и пул.
Диагностика
use Flytachi\Winter\Redis\RedisPool;
$config = RedisPool::config(MainRedisConfig::class);
$config->getDsn(); // 'redis://localhost:6379/0' — без пароля, можно логировать
$config->ping(); // true | false, не бросает
$config->pingDetail(); // ['status' => true, 'latency' => 0.32, 'error' => null]pingDetail() — то, что отдают в /actuator/health: он не только отвечает «жив ли»,
но и показывает задержку, а при отказе — текст ошибки.
Экземпляр из config() — не тот, что в пуле
RedisPool::config() возвращает регистрационный экземпляр: он существует, чтобы
читать настройки. Соединения держат другие экземпляры — по одному на слот пула, и
именно их проверяет и закрывает пул.
Дальше
- Сторы — как этим пользоваться в прикладном коде
- Пул соединений — сколько соединений и что при переполнении