Сторы
Стор — не структура Redis, а способ разделить одну базу между частями приложения: префикс ключей плюс те команды, которыми пользуются чаще всего. Ниже — зачем он нужен, и разбор каждого метода: что принимает, что возвращает, чем бросается.
Что такое стор и зачем
Проблема. В Redis нет пространств имён. Все ключи лежат в одной плоской базе, и ключ
42, записанный модулем сессий, ничем не отличается от ключа 42, записанного очередью
задач: кто пришёл вторым, тот и затёр. Разводят их единственным способом — договариваются
об именах. Договорённость живёт в комментариях и в памяти команды, а строка 'session:'
дублируется по всему коду, и однажды кто-то напишет 'sessions:'.
Решение. Договорённость становится классом. Стор знает свой префикс и приписывает его
сам — в коде остаётся короткое имя ключа, а полное собирается в одном месте. Заодно
появляется граница: keys() и flush() работают в пределах стора, а не всей базы.
<?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;
class AuthService
{
#[Autowired]
private SessionStore $sessions;
public function remember(int $userId, string $token): void
{
$this->sessions->set((string) $userId, $token, ttl: 3600);
}
}Стор не держит соединения: каждый вызов спрашивает у пула клиента текущей единицы работы. Поэтому стор можно спокойно хранить в поле синглтона, хотя соединение хранить там нельзя.
Что даёт префикс
$sessions->set('42', 'token'); // на сервере: session:42
$queue->set('42', 'job'); // на сервере: queue:42
$sessions->get('42'); // 'token'
$queue->get('42'); // 'job'Два стора на одной базе не мешают друг другу, а flush() и keys()
работают в границах своего стора. Префикс — это просто начало имени ключа, не отдельная
сущность на сервере: он должен быть уникален в пределах базы, а двоеточие в конце —
привычка, а не требование.
Стор без префикса допустим — например, когда приложение единственный владелец базы. Но
тогда flush() работать откажется: без префикса под шаблон попадёт всё,
включая ключи, о которых вы не знаете.
Справочник
RedisStore
RedisStore — абстрактный класс, от которого наследуют сторы приложения. Он не
создаётся сам по себе: смысл стора в двух свойствах, которые задаёт наследник.
$redisConfigClassName — класс конфигурации, то есть точка подключения. Обязателен.
$prefix — строка, приписываемая к каждому ключу. По умолчанию пустая.
Шестнадцать методов класса делятся на четыре группы: работа со значениями, счётчики, обзор содержимого и выход на уровень ниже — ручки структур и сырой клиент.
Значения
get()
Читает значение по ключу.
Возвращает то, что лежит в ключе, и null — когда ключа нет. Драйвер на этом месте
отдаёт false, что неотличимо от сохранённого пустого значения; null эту
двусмысленность снимает хотя бы для отсутствия.
Синтаксис
public function get(string $key): mixedПараметры
$key — имя ключа без префикса; полное имя соберёт сам стор.
Возвращает
Значение, либо null, если ключа нет.
Ошибки
RedisCommandException — если ключ занят структурой другого типа: списком, хешем,
стримом. Это ошибка в коде, а не отсутствие данных, поэтому она не притворяется null.
Пример
$store->get('token'); // 'abc123'
$store->get('nothing'); // nullРезультат при неверном типе
RedisCommandException: WRONGTYPE Operation against a key holding the wrong kind of valueЧто вернётся, если сохранить `false`
Зависит от сериализатора, и оба случая стоит знать.
Без сериализатора (умолчание) false уходит на сервер как пустая строка, и get()
вернёт '' — не false и не null.
С настроенным сериализатором значение переживает поездку и приходит от драйвера как
false, а стор превращает false в null — то есть от отсутствующего ключа его уже не
отличить.
Если нужно хранить именно логическое значение, кодируйте его ('0' / '1') или
проверяйте наличие отдельно через has().
set()
Записывает значение, при необходимости — со сроком жизни.
Повторная запись существующего ключа снимает с него срок, если новый не задан, — как
и обычный SET в Redis.
Синтаксис
public function set(string $key, mixed $value, ?int $ttl = null): boolПараметры
$key — имя ключа без префикса.
$value — значение. С сериализатором по умолчанию это должна быть строка или число:
сервер хранит байты как есть, и массив превратится в строку "Array". Чтобы хранить
массивы и объекты, нужен сериализатор — см.
Конфигурация.
$ttl — срок жизни в секундах, отсчитываемый от текущего момента. По умолчанию
null — ключ живёт, пока его не удалят.
Возвращает
true, если запись выполнена.
Ошибки
LogicException — если $ttl меньше или равен нулю. Redis такой срок не примет, а тихое
false в ответ выглядело бы как «не записалось по неизвестной причине»:
LogicException: A ttl must be a positive number of seconds; got 0.
To remove a key, call delete().Пример
$store->set('token', $jwt); // навсегда
$store->set('token', $jwt, ttl: 3600); // на час`ttl` — это длительность, а не момент времени
ttl: 60 означает «шестьдесят секунд от текущего момента». ttl: time() + 60 просит
примерно пятьдесят шесть лет, и никто об этом не сообщит: ключ просто никогда не
протухнет. Проявится это не сразу и будет выглядеть как утечка памяти на сервере.
Абсолютного момента в API стора нет намеренно — два похожих смысла в одном аргументе как раз и порождают такую путаницу. Если нужен именно момент, он берётся у драйвера:
$store->set('session', $data);
$store->raw()->expireAt($store->key('session'), $expiresAt);has()
Проверяет, существует ли ключ.
В отличие от get() !== null значение не передаётся по сети — на больших значениях это
заметно. И это единственный способ отличить сохранённое пустое значение от отсутствия
ключа.
Синтаксис
public function has(string $key): boolПараметры
$key — имя ключа без префикса.
Возвращает
true, если ключ существует.
Пример
$store->has('token'); // true
$store->has('nothing'); // falsedelete()
Удаляет один или несколько ключей.
Удаление отсутствующего ключа ошибкой не считается. Возвращаемое число полезно, когда нужно знать, была ли работа проделана на самом деле: «сессия действительно существовала».
Синтаксис
public function delete(string ...$keys): intПараметры
...$keys — имена ключей без префикса, сколько угодно. Можно не передать ни одного —
тогда запрос на сервер не уйдёт вовсе.
Возвращает
Число ключей, которые существовали и были удалены.
Пример
$store->delete('token'); // 1
$store->delete('token'); // 0 — его уже нет
$store->delete('a', 'b', 'c'); // 2, если 'c' не существовал
$store->delete(); // 0, без обращения к серверуttl()
Сообщает, сколько ключу осталось жить.
Синтаксис
public function ttl(string $key): ?intПараметры
$key — имя ключа без префикса.
Возвращает
Оставшиеся секунды, либо null — и когда срок не задан, и когда ключа нет. Если
различать эти два случая важно, спросите has().
Пример
$store->set('token', $jwt, ttl: 3600);
$store->ttl('token'); // 3600
$store->ttl('forever'); // null — ключ есть, срока нет
$store->ttl('nothing'); // null — ключа нетСчётчики
increment()
Атомарно увеличивает числовое значение.
Это одна команда на сервере, а не «прочитать, прибавить, записать»: два одновременных
запроса не могут оба прочитать 10 и оба записать 11. Именно поэтому счётчики держат в
Redis, а не в переменной приложения.
Синтаксис
public function increment(string $key, int $by = 1): intПараметры
$key — имя ключа без префикса. Если ключа не было, он создаётся со значением 0 и
сразу увеличивается.
$by — на сколько увеличить. По умолчанию 1.
Возвращает
Новое значение.
Ошибки
RedisCommandException — если в ключе лежит не число:
$store->set('name', 'Alice');
$store->increment('name');RedisCommandException: ERR value is not an integer or out of rangeПример
$store->increment('hits'); // 1
$store->increment('hits', 10); // 11Счётчик с окном
// Не больше ста обращений в минуту от одного пользователя
$key = "rate:{$userId}";
$hits = $store->increment($key);
if ($hits === 1) {
$store->raw()->expire($store->key($key), 60); // окно заводится вместе со счётчиком
}
if ($hits > 100) {
throw new TooManyRequests();
}decrement()
Атомарно уменьшает числовое значение.
Полная противоположность increment() и во всём остальном идентичен ему:
та же атомарность, то же создание отсутствующего ключа из нуля, то же исключение на
нечисловом значении.
Синтаксис
public function decrement(string $key, int $by = 1): intПараметры
$key — имя ключа без префикса.
$by — на сколько уменьшить. По умолчанию 1.
Возвращает
Новое значение. Уйти ниже нуля оно может — ограничения снизу нет.
Ошибки
RedisCommandException — если в ключе лежит не число.
Пример
$store->increment('hits', 10); // 10
$store->decrement('hits'); // 9
$store->decrement('hits', 3); // 6Обзор содержимого
keys()
Перечисляет ключи стора.
Инструмент инвентаризации: посмотреть, что лежит, собрать список для удаления, показать содержимое в админке. Для выборки данных он не нужен — за значением ходят по известному имени.
Синтаксис
public function keys(string $pattern = '*'): arrayПараметры
$pattern — glob-шаблон, действующий внутри стора: префикс подставляется сам, так
что 'user:*' найдёт session:user:1, а не всё подряд в базе. По умолчанию '*' — все
ключи стора.
Возвращает
Массив имён без префикса — то есть ровно в том виде, в каком их принимают
get() и delete(). Для пустого стора — пустой массив.
Пример
$store->keys(); // ['42', 'abc']
$store->keys('user:*'); // только совпавшее, по-прежнему внутри стораЭто `SCAN`, а не `KEYS` — и разница не косметическая
KEYS обходит весь keyspace одной командой, а Redis однопоточен: на большой базе он
останавливает всех клиентов на всё время обхода. SCAN идёт пачками и даёт серверу
дышать между ними.
Плата — результат собирается по частям: ключ, добавленный во время обхода, может в него попасть, а может и нет; удалённый — может ещё успеть попасть в список. Для инвентаризации это нормально, для точного счётчика — нет.
Весь результат материализуется в памяти, поэтому на сторе с миллионами ключей стоит задать узкий шаблон.
flush()
Удаляет все ключи стора.
Внутри — тот же SCAN пачками с удалением, поэтому на большом сторе это обход, а не
мгновенная операция. Зато сервер не блокируется.
Синтаксис
public function flush(): intПараметры
Нет.
Возвращает
Число удалённых ключей.
Ошибки
LogicException — если у стора нет префикса.
Пример
$sessions->flush(); // 128 — соседние сторы не тронутыСтор без префикса очистить нельзя
LogicException: Main\Stores\LegacyStore::flush() needs a $prefix —
without one it would delete the whole database.Это отказ, а не перестраховка: без префикса под шаблон попадает вся база, включая чужие
ключи. Очистить базу целиком — осознанное действие, и делается оно явно:
raw()->flushDB().
Структуры
Три метода отдают ручку — объект, через который работают с одной структурой данных. Общее у них одно и главное: ни один ничего не отправляет на сервер. Это взгляд на ключ, а не запрос, поэтому получить ручку на несуществующий ключ можно и это ничего не стоит.
Префикс применяется в момент получения ручки — дальше его негде забыть. В этом и разница
с raw(), где о нём приходится помнить на каждой команде.
hash()
Возвращает ручку на хеш, лежащий под этим ключом.
Хеш — это словарь внутри одного ключа: поля и их значения. Подробно — Хеши.
Синтаксис
public function hash(string $key): RedisHashПараметры
$key — имя ключа без префикса.
Возвращает
RedisHash, привязанный к полному имени ключа.
Пример
$store->hash('cart:42')->set('qty', '2');list()
Возвращает ручку на список, лежащий под этим ключом.
Список — упорядоченная последовательность с быстрыми операциями по краям; на нём делают очереди. Подробно — Списки.
Синтаксис
public function list(string $key): RedisListПараметры
$key — имя ключа без префикса.
Возвращает
RedisList, привязанный к полному имени ключа.
Пример
$store->list('jobs')->push($payload);stream()
Возвращает ручку на стрим, лежащий под этим ключом.
Стрим — журнал записей, который читают несколько потребителей независимо друг от друга. Подробно — Стримы.
Синтаксис
public function stream(string $key): RedisStreamПараметры
$key — имя ключа без префикса.
Возвращает
RedisStream, привязанный к полному имени ключа.
Пример
$store->stream('events')->add(['type' => 'signup']);Прямой доступ
raw()
Возвращает клиента текущей единицы работы.
У Redis сотни команд, и обернуть их все — работа без конца. Всё, чего нет в сторе и в
ручках, берётся отсюда: сортированные множества, pub/sub, SETNX, скрипты на Lua.
Синтаксис
public function raw(): \RedisПараметры
Нет.
Возвращает
Объект \Redis — клиента расширения ext-redis, уже подключённого и выбравшего нужную
базу.
Пример
$store->raw()->zAdd($store->key('leaders'), 100, 'user:1');
$store->raw()->publish('events', $payload);Префикс здесь не применяется — и это тихая ошибка
$store->raw()->zAdd('leaders', ...) не выбросит исключения и не вернёт ошибку. Ключ
просто запишется мимо стора, и всё будет выглядеть работающим.
Последствия появятся позже и в другом месте: keys() и get() этот
ключ не увидят, flush() его не удалит, а два стора, оба забывшие префикс,
тихо поделят одно имя — ровно та коллизия, ради предотвращения которой префикс и
заводился. Забытый key() не ломает вызов, он отменяет единственную гарантию
стора.
Правило простое: каждый ключ, попадающий в raw(), оборачивается в
key() — или, если у структуры один ключ на всю работу, берите ручку
hash() или list(), где префикс уже применён.
Сам клиент действителен только в пределах текущего запроса: под Swoole он уходит обратно в пул, когда корутина завершилась. Сохранённый в поле долгоживущего объекта, он раздаст один сокет всем последующим запросам — ровно то, ради предотвращения чего существует пул.
key()
Собирает полное имя ключа — то, которое увидит сервер.
Нужен везде, где ключ уходит в raw() или в transaction(): там
префикс не применяется.
Синтаксис
public function key(string $name): stringПараметры
$name — имя ключа без префикса.
Возвращает
Строку «префикс + имя».
Пример
$store->key('42'); // 'session:42'
$store->key(''); // 'session:' — сам префиксtransaction()
Выполняет колбэк на одном и том же соединении от начала до конца.
Это единственное место, где автоматического возврата соединения в пул недостаточно.
MULTI и pipeline() держат состояние на соединении: сервер запоминает начатую
последовательность и ждёт EXEC по тому же сокету. Если середина последовательности
уедет на другое соединение, команды разъедутся по двум сокетам и результат будет
непредсказуем.
Синтаксис
public function transaction(callable $callback): mixedПараметры
$callback — функция, получающая сырого клиента \Redis. Всё, что она вызовет, уйдёт по
одному соединению.
Возвращает
То, что вернул колбэк, без изменений.
Пример
$result = $store->transaction(function (\Redis $redis) use ($store) {
$redis->multi();
$redis->incr($store->key('a'));
$redis->incr($store->key('b'));
return $redis->exec();
}); // [1, 1]Ключи здесь не префиксуются автоматически — в колбэк приходит сырой клиент, поэтому
key() обязателен, как и в raw().
Транзакция Redis — не транзакция базы данных
MULTI/EXEC гарантируют, что команды выполнятся подряд и без чужих команд между ними.
Отката нет: если одна из команд не сработает, остальные всё равно применятся. Проверять
условия нужно до последовательности, а не после.
configClass()
Возвращает класс конфигурации, к которому привязан стор.
Нужен для диагностики и для тех мест, где по стору надо узнать точку подключения:
например, RedisPool::stats() выдаёт занятость с
ключами по именам конфигураций.
Синтаксис
public function configClass(): stringПараметры
Нет.
Возвращает
Полное имя класса конфигурации.
Что стор ожидает от значений
С сериализатором по умолчанию (SERIALIZER_NONE) значение должно быть строкой или
числом: сервер хранит присланные байты как есть, а PHP приводит к строке всё остальное.
$store->set('user', ['id' => 1]); // в базе строка "Array", только PHP-warning
$store->get('user'); // 'Array'
$store->set('n', 42);
$store->get('n'); // '42' — строка, не intЧтобы хранить массивы и объекты и получать их обратно теми же типами, задайте сериализатор в конфигурации. Он применяется к соединению, поэтому распространяется и на значения полей хеша, и на элементы списков.
Соответствие командам Redis
| Метод | Команда |
|---|---|
get() / set() |
GET / SET, со сроком — SETEX |
has() / delete() |
EXISTS / DEL |
increment() / decrement() |
INCRBY / DECRBY |
ttl() |
TTL |
keys() |
SCAN, не KEYS |
flush() |
SCAN + DEL |
hash() / list() / stream() |
— (ничего не отправляют) |
key() / raw() / configClass() |
— |
transaction() |
что вызовет колбэк |
Дальше
- Хеши — поля, их сроки жизни и требования к версии сервера
- Списки — очереди и блокирующее чтение
- Стримы — журнал событий и группы потребителей
- Конфигурация — точка подключения, база, сериализатор
- Пул соединений — откуда берётся клиент и когда возвращается