Redis · Конфигурация

Конфигурация

Конфигурация описывает одну точку подключения: куда идти, чем аутентифицироваться, какую базу выбрать. Из неё пул строит все свои соединения, поэтому всё, что должно быть одинаковым у каждого соединения, живёт здесь.

Класс конфигурации

Наследуется от RedisConfig, заполняется в setUp():

main/Configurations/MainRedisConfig.php
<?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, и это выглядит как обрыв, а не как таймаут. Это про сервер, который принял команду и молчит.

php
$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:

php
$this->password = env('REDIS_PASS', '');

Начиная с Redis 6 есть ACL: отдельные пользователи со своими правами и доступом к своему набору ключей. Управляемые Redis у облачных провайдеров часто выдают именно такого пользователя, а не пароль от default:

php
$this->username = env('REDIS_USER', 'app');
$this->password = env('REDIS_PASS');

Если $username пуст, отправляется AUTH <пароль>; если задан — AUTH <пользователь> <пароль>. Проверить, кем вы подключились, можно так:

php
$config->connection()->rawCommand('ACL', 'WHOAMI');   // 'app'

Пароль не попадает в логи пакета

getDsn() намеренно собирается из хоста, порта и базы — ни пароля, ни пользователя в нём нет, поэтому строку можно писать в лог целиком. Пул логирует именно её.

TLS и unix-сокет

Схема указывается прямо в адресе — драйвер разбирает её сам:

php
$this->host = 'tls://redis.example.com';
$this->port = 6380;

Материалы для TLS — доверенный корневой сертификат, клиентский сертификат, требования к проверке узла — передаются через $context:

php
$this->host    = 'tls://redis.example.com';
$this->port    = 6380;
$this->context = [
  'stream' => [
      'cafile'      => '/etc/ssl/certs/redis-ca.pem',
      'verify_peer' => true,
  ],
];

Для локального сокета:

php
$this->host = '/var/run/redis/redis.sock';
$this->port = 0;                                  // порт не используется

Ошибки TLS приходят предупреждениями, а не исключениями

Драйвер сообщает о неудачном рукопожатии PHP-warning’ом (Failed to enable crypto), и соединение при этом может считаться установленным. Проверяйте настройку явным ping() при первом деплое, а не по отсутствию исключений.

Несколько конфигураций

Конфигурация — это не «настройки Redis в приложении», а одна точка подключения. Их бывает несколько, и это нормально:

php
class MainRedisConfig extends RedisConfig    { /* база 0 — сессии, кеш */ }
class QueueRedisConfig extends RedisConfig   { /* база 1 — очереди          */ }
class MetricsRedisConfig extends RedisConfig { /* другой сервер целиком     */ }

Каждая получает собственный пул со своим потолком, и в RedisPool::stats() они видны по отдельности. Сторы привязываются к нужной через $redisConfigClassName.

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

Номер базы

Задаётся в конфигурации и только там:

php
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 и приложения на других языках.

Платой за это становится то, что значение должно быть строкой или числом:

php
$store->set('user', ['id' => 1]);   // → в базе строка "Array", и только PHP-warning
$store->get('user');                 // 'Array'

Ошибка не выбрасывается — поэтому выбор сериализатора и сделан свойством конфигурации, где он виден один раз и на месте:

php
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 секунды. Чтобы задать своё, конфигурация объявляет себя пул-осведомлённой:

main/Configurations/MainRedisConfig.php
<?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-вариант:

scripts/warmup.php
<?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');

Каждый такой объект открывает своё соединение и закрывает его, когда его соберёт сборщик мусора. В приложении, обслуживающем запросы, так делать не нужно — там конфигурация и пул.

Диагностика

php
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() возвращает регистрационный экземпляр: он существует, чтобы читать настройки. Соединения держат другие экземпляры — по одному на слот пула, и именно их проверяет и закрывает пул.

Дальше

  • Сторы — как этим пользоваться в прикладном коде
  • Пул соединений — сколько соединений и что при переполнении