Сторы
Стор — не структура Redis, а способ разделить одну базу между частями приложения: префикс ключей плюс те команды, которыми пользуются чаще всего. Ниже — зачем он нужен, и разбор каждого метода с аргументами и примерами.
Что такое стор
В Redis нет пространств имён: все ключи лежат в одной плоской базе, и 42 от сессии
неотличим от 42 от очереди. Единственный способ их развести — договориться об именах.
Стор превращает эту договорённость в код.
<?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() работать откажется: без префикса под шаблон попадёт всё, включая ключи,
о которых вы не знаете.
Справочник методов
Значения
get()
public function get(string $key): mixedЧитает значение по ключу.
| Аргумент | Тип | По умолчанию | Что делает |
|---|---|---|---|
$key |
string |
— | имя ключа без префикса |
Возвращает значение либо null, если ключа нет. Драйвер на этом месте отдаёт
false, что неотличимо от сохранённого false; null эту двусмысленность снимает.
Бросает RedisCommandException, если ключ занят структурой другого типа — списком,
хешем. Это ошибка в коде, а не отсутствие данных, поэтому она не притворяется null.
$store->get('token'); // 'abc123'
$store->get('nothing'); // nullЕсли нужно хранить именно false
Сохранённое false вернётся как null — различить их через get() нельзя. Храните
такие значения закодированными ('0'/'1') или проверяйте наличие отдельно через
has().
set()
public function set(string $key, mixed $value, ?int $ttl = null): boolЗаписывает значение, при необходимости — со сроком жизни.
| Аргумент | Тип | По умолчанию | Что делает |
|---|---|---|---|
$key |
string |
— | имя ключа без префикса |
$value |
mixed |
— | значение. Строка или число; для остального нужен сериализатор |
$ttl |
?int |
null |
срок жизни в секундах от текущего момента. null — без срока |
Возвращает true, если запись выполнена. Бросает LogicException, если $ttl
меньше или равен нулю: Redis такой срок не примет, а тихое false в ответ выглядело бы
как «не записалось по неизвестной причине».
$store->set('token', $jwt); // навсегда
$store->set('token', $jwt, ttl: 3600); // на час
$store->set('token', $jwt, ttl: 0); // LogicExceptionttl — это длительность, а не момент времени
ttl: 60 означает «шестьдесят секунд от текущего момента». ttl: time() + 60 просит
примерно пятьдесят шесть лет, и никто об этом не сообщит: ключ просто никогда не
протухнет. Проявится это не сразу и будет выглядеть как утечка памяти на сервере.
Абсолютного момента в API стора нет намеренно — два похожих смысла в одном аргументе как раз и порождают такую путаницу. Если нужен именно момент:
$store->set('session', $data);
$store->raw()->expireAt($store->key('session'), $expiresAt);Повторная запись существующего ключа снимает с него срок, если новый не задан, —
как и обычный SET в Redis.
has()
public function has(string $key): bool| Аргумент | Тип | По умолчанию | Что делает |
|---|---|---|---|
$key |
string |
— | имя ключа без префикса |
Возвращает true, если ключ существует. В отличие от get() !== null, значение не
передаётся по сети — для больших значений это заметно, и это единственный способ
отличить сохранённое false от отсутствия.
delete()
public function delete(string ...$keys): intУдаляет один или несколько ключей.
| Аргумент | Тип | По умолчанию | Что делает |
|---|---|---|---|
...$keys |
string |
— | имена ключей без префикса; можно ни одного |
Возвращает число ключей, которые существовали и были удалены. Без аргументов — 0,
запрос не отправляется.
$store->delete('token'); // 1
$store->delete('token'); // 0 — его уже нет
$store->delete('a', 'b', 'c'); // 2, если 'c' не существовалУдаление отсутствующего ключа — не ошибка. Возвращаемое число полезно, когда нужно знать, была ли работа проделана: например, «сессия действительно существовала».
Счётчики
increment() и decrement()
public function increment(string $key, int $by = 1): int
public function decrement(string $key, int $by = 1): intАтомарно изменяют числовое значение.
| Аргумент | Тип | По умолчанию | Что делает |
|---|---|---|---|
$key |
string |
— | имя ключа без префикса |
$by |
int |
1 |
на сколько изменить |
Возвращают новое значение. Если ключа не было, он создаётся со значением 0 и сразу
изменяется. Бросают RedisCommandException, если в ключе лежит не число.
$store->increment('hits'); // 1
$store->increment('hits', 10); // 11
$store->decrement('hits'); // 10Это одна команда на сервере, а не «прочитать, прибавить, записать»: два одновременных
запроса не могут оба прочитать 10 и оба записать 11. Именно поэтому счётчики держат
в Redis, а не в переменной приложения.
// счётчик обращений с ограничением по времени
$key = "rate:{$userId}";
$hits = $store->increment($key);
if ($hits === 1) {
$store->raw()->expire($store->key($key), 60); // окно в минуту
}
if ($hits > 100) {
throw new TooManyRequests();
}$store->set('name', 'Alice');
$store->increment('name'); // RedisCommandException: value is not an integerРаньше такой вызов возвращал 0 — отказ выглядел как «счётчик обнулился». Теперь это
исключение.
ttl()
public function ttl(string $key): ?int| Аргумент | Тип | По умолчанию | Что делает |
|---|---|---|---|
$key |
string |
— | имя ключа без префикса |
Возвращает оставшиеся секунды жизни либо null — и когда срок не задан, и когда
ключа нет. Если различать эти случаи важно, спросите has().
$store->set('token', $jwt, ttl: 3600);
$store->ttl('token'); // 3600
$store->ttl('forever'); // null — ключ есть, срока нет
$store->ttl('nothing'); // null — ключа нетОбзор содержимого
keys()
public function keys(string $pattern = '*'): arrayПеречисляет ключи стора.
| Аргумент | Тип | По умолчанию | Что делает |
|---|---|---|---|
$pattern |
string |
'*' |
glob-шаблон, действующий внутри стора |
Возвращает массив имён без префикса — то есть в том виде, в каком их принимают
get() и delete(). Для пустого стора — пустой массив.
$store->keys(); // ['42', 'abc']
$store->keys('user:*'); // только совпавшее, по-прежнему внутри стораЭто SCAN, а не KEYS — и разница не косметическая
KEYS обходит весь keyspace одной командой, а Redis однопоточен: на большой базе он
останавливает всех клиентов на всё время обхода. SCAN идёт пачками и даёт серверу
дышать между ними.
Плата — результат собирается по частям: ключ, добавленный во время обхода, может в него попасть, а может и нет; удалённый — может ещё успеть попасть в список. Для инвентаризации это нормально, для точного счётчика — нет.
Весь результат материализуется в памяти, поэтому на сторе с миллионами ключей стоит задать узкий шаблон.
flush()
public function flush(): intУдаляет все ключи стора. Аргументов нет.
Возвращает число удалённых ключей. Бросает LogicException, если у стора нет
префикса.
$sessions->flush(); // 128 — соседние сторы не тронутыВнутри — тот же SCAN пачками с удалением, поэтому на большом сторе это обход, а не
мгновенная операция. Зато сервер не блокируется.
Стор без префикса очистить нельзя
LogicException: Main\Stores\LegacyStore::flush() needs a $prefix —
without one it would delete the whole database.Это отказ, а не перестраховка: без префикса под шаблон попадает вся база, включая чужие
ключи. Если очистить базу целиком — это осознанное действие: raw()->flushDB().
Структуры
hash(), list() и stream()
public function hash(string $key): RedisHash
public function list(string $key): RedisList
public function stream(string $key): RedisStreamВозвращают ручку, привязанную к ключу: на хеш, на список или на стрим.
| Аргумент | Тип | По умолчанию | Что делает |
|---|---|---|---|
$key |
string |
— | имя ключа без префикса |
Возвращают объект-представление. Ни один из вызовов ничего не отправляет на сервер: это взгляд на ключ, а не запрос.
$store->hash('cart:42')->set('qty', '2');
$store->list('jobs')->push($payload);
$store->stream('events')->add(['type' => 'signup']);Префикс применяется в момент получения ручки — дальше его негде забыть. В этом и разница
с raw(), где о нём приходится помнить.
Прямой доступ
raw()
public function raw(): \RedisВозвращает клиента текущей единицы работы. Аргументов нет.
У Redis сотни команд, и обернуть их все — работа без конца. Всё, чего нет в сторе и в
ручках, берётся отсюда: сортированные множества, pub/sub, SETNX, скрипты.
$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()
public function key(string $name): string| Аргумент | Тип | По умолчанию | Что делает |
|---|---|---|---|
$name |
string |
— | имя ключа без префикса |
Возвращает полное имя с префиксом — то, что видит сервер. Нужно везде, где ключ
уходит в raw().
$store->key('42'); // 'session:42'
$store->key(''); // 'session:' — сам префиксtransaction()
public function transaction(callable $callback): mixedВыполняет колбэк на одном и том же соединении от начала до конца.
| Аргумент | Тип | По умолчанию | Что делает |
|---|---|---|---|
$callback |
callable(\Redis): mixed |
— | получает сырого клиента, его результат возвращается наружу |
Возвращает то, что вернул колбэк.
Это единственное место, где автоматического возврата соединения в пул недостаточно:
MULTI и pipeline() держат состояние на соединении, поэтому все команды
последовательности обязаны попасть в один сокет.
$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()
public function configClass(): stringКласс конфигурации, к которому привязан стор. Аргументов нет. Нужен для диагностики и
для RedisPool::stats(), где ключи — имена конфигураций.
Что стор ожидает от значений
С сериализатором по умолчанию (SERIALIZER_NONE) значение должно быть строкой или
числом:
$store->set('user', ['id' => 1]); // в базе строка "Array", только PHP-warning
$store->get('user'); // 'Array'Чтобы хранить массивы и объекты, задайте сериализатор в конфигурации. Он применяется к соединению, поэтому распространяется и на значения полей хеша, и на элементы списков.
Соответствие командам 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() |
— |
transaction() |
что вызовет колбэк |
Дальше
- Хеши — поля, их сроки жизни и требования к версии сервера
- Списки — очереди и блокирующее чтение
- Стримы — журнал событий и группы потребителей
- Пул соединений — откуда берётся клиент и когда возвращается