Хеши
Хеш — словарь «поле → значение» под одним ключом. Это способ хранить объект целиком, не размазывая его по десятку ключей и не перечитывая целиком ради одного поля. Ниже — что это за структура, где она уместна, и разбор каждого метода.
Что такое хеш и зачем
Проблема. Объект приходится куда-то класть. Разложить по отдельным ключам —
user:42:name, user:42:email, user:42:plan — значит завести три ключа вместо одного,
трижды заплатить накладными расходами Redis на ключ и потерять возможность удалить
объект одной командой. Сложить в один ключ строкой JSON — значит перечитывать и
переписывать весь объект ради одного поля, а два одновременных обновления разных полей
затрут друг друга.
Решение. Хеш — контейнер «поле → значение» внутри одного ключа. Поле читается и пишется отдельно от остальных, объект удаляется целиком одной командой, а память Redis тратится как на один ключ, а не на десять.
$cart = $store->hash('cart:42');
$cart->setAll(['qty' => '2', 'sku' => 'A-1']);
$cart->increment('qty'); // 3 — соседние поля не трогали
$cart->get('sku'); // 'A-1'Свойства структуры
Хеш — это отображение строк в строки внутри одного ключа Redis: cart:42 содержит поля
qty, sku, total. Свойства, из которых следует всё остальное:
- Доступ к полю — O(1), независимо от того, сколько в хеше полей.
- Значения — строки, как и везде в Redis. Вложенных структур нет: поле не может содержать другой хеш или список.
- Порядок полей не определён.
fields()вернёт их в том порядке, в каком удобно серверу, а не в порядке добавления. - Полей может быть до 4 294 967 295.
Маленькие хеши хранятся особым компактным представлением (listpack) и занимают заметно
меньше памяти, чем те же данные в отдельных ключах — на текущих версиях порог составляет
512 полей (hash-max-listpack-entries). Это и есть главный практический аргумент за хеш:
тысяча объектов по десять полей в виде хешей дешевле десяти тысяч отдельных ключей.
Как и список, хеш не нужно создавать: запись первого поля создаёт ключ, удаление последнего — удаляет.
Где принято применять
Объект или запись. Профиль пользователя, корзина, карточка заказа. Одно поле читается и обновляется без чтения и перезаписи всего объекта — в отличие от JSON в обычном ключе.
Сгруппированные счётчики. Просмотры по дням, метрики по эндпоинтам, лимиты по
клиентам: hash('stats:2026-08') с полями-датами. increment() атомарен на
уровне поля.
Настройки и флаги. Небольшой набор параметров, которые читают целиком
(all()) и меняют по одному.
Данные сессии. Поля читаются выборочно, а на новых версиях сервера отдельные поля могут ещё и жить своим сроком.
Где хеш — неправильный выбор:
| Задача | Почему не хеш | Что вместо |
|---|---|---|
| Найти объекты по значению поля | хеш не индексируется, придётся перебирать | вторичный индекс на множествах |
| Хранить вложенную структуру | значения — только строки | сериализация или отдельные ключи |
| Сортировать по значению | порядка нет | сортированное множество (ZSET) |
| Хранить очередь или историю | поля не упорядочены | список или стрим |
| Сотни тысяч полей с частым обходом | all() тянет всё разом |
несколько ключей или HSCAN |
Справочник
RedisHash
RedisHash — ручка на один хеш: объект, через который выполняются команды над этим
ключом. Получают её у стора:
$cart = $store->hash('cart:42'); // сервер увидит 'session:cart:42'hash() ничего не выполняет — это представление, а не
запрос. Префикс применяется здесь один раз, поэтому дальше его негде забыть. Соединения
ручка не держит: каждый вызов спрашивает клиента у стора, поэтому её можно свободно
передавать в пределах запроса.
Все методы, кроме перечисленных в разделе Ключ целиком, работают с
полями. Это же относится и к аргументу $ttl: он задаёт срок тому, что пишет вызов,
— то есть полю, а не ключу.
Чтение
get()
Читает значение одного поля.
Синтаксис
public function get(string $field): mixedПараметры
$field — имя поля.
Возвращает
Значение, либо null — и если поля нет, и если нет самого хеша. Драйвер в обоих случаях
отдаёт false; null выбран, чтобы «нет значения» не путалось с сохранённым false.
Ошибки
RedisCommandException — если ключ занят структурой другого типа, например списком. Это
ошибка в коде, а не отсутствие данных, поэтому она не притворяется null.
Пример
$cart->get('qty'); // '2'
$cart->get('nothing'); // nullgetMany()
Читает несколько полей за один обмен с сервером.
Полнота результата здесь не мелочь: если бы отсутствующие поля просто выпадали, по ответу нельзя было бы понять, какое именно пропало, не сравнивая массивы.
Синтаксис
public function getMany(string ...$fields): arrayПараметры
...$fields — имена полей, сколько угодно. Можно не передать ни одного.
Возвращает
Массив «поле → значение», в котором всегда есть все запрошенные имена: отсутствующие
приходят как null. Без аргументов — пустой массив.
Пример
$cart->getMany('qty', 'sku', 'promo');Результат
['qty' => '2', 'sku' => 'A-1', 'promo' => null]Разбор в переменные
// три поля — один запрос к серверу, а не три
['name' => $name, 'email' => $email] = $profile->getMany('name', 'email');all()
Читает весь хеш целиком.
Синтаксис
public function all(): arrayПараметры
Нет.
Возвращает
Массив «поле → значение»; для несуществующего ключа — пустой массив.
Пример
$cart->all(); // ['qty' => '2', 'sku' => 'A-1']
$missing->all(); // []fields()
Читает только имена полей.
Дешевле all(), когда значения не нужны: например, чтобы узнать, за какие дни в
счётчике вообще есть данные.
Синтаксис
public function fields(): arrayПараметры
Нет.
Возвращает
Массив имён; для несуществующего ключа — пустой. Порядок не определён.
Пример
$cart->fields(); // ['qty', 'sku']values()
Читает только значения полей.
Синтаксис
public function values(): arrayПараметры
Нет.
Возвращает
Массив значений; для несуществующего ключа — пустой.
Порядок не определён, но он тот же, что у fields(): значения
соответствуют именам позиция в позицию, если оба вызова сделаны без изменений между ними.
Пример
$cart->fields(); // ['qty', 'sku']
$cart->values(); // ['2', 'A-1']has()
Проверяет, существует ли поле.
Отличается от get() !== null тем, что не передаёт значение по сети, — а для большого
значения это заметно.
Синтаксис
public function has(string $field): boolПараметры
$field — имя поля.
Возвращает
true, если поле существует.
count()
Сообщает число полей.
Синтаксис
public function count(): intПараметры
Нет.
Возвращает
Количество полей; для несуществующего ключа — 0. Операция O(1): сервер хранит это число
и не пересчитывает его.
Пример
if ($cart->count() === 0) {
return null; // корзины нет
}Запись
set()
Записывает одно поле, при необходимости — со сроком жизни этого поля.
С $ttl это одна команда HSETEX, а не запись с последующим HEXPIRE. Разница
существенная: между двумя командами есть щель, в которой поле уже есть, а срока у него
ещё нет, — и если в этот момент что-то сорвётся, поле останется навсегда.
Синтаксис
public function set(string $field, mixed $value, ?int $ttl = null): boolПараметры
$field — имя поля.
$value — значение. Строка или число; чтобы записать массив или объект, нужен
сериализатор — см.
Конфигурация.
$ttl — срок жизни поля в секундах от текущего момента. По умолчанию null — поле живёт,
пока его не удалят.
Возвращает
true, если команда выполнена.
Ошибки
LogicException — при $ttl меньше или равном нулю. Чтобы убрать поле, есть
delete().
RedisFeatureException — если сервер не умеет HSETEX; см.
Требования к версии.
Пример
$cart->set('qty', '2');
$cart->set('lock', '1', ttl: 30); // это поле исчезнет через полминутыПерезапись без `ttl` снимает срок с поля
Проверено на живом сервере: поле со сроком в сто секунд после set('lock', '2') без
ttl теряет срок и остаётся навсегда. Так устроен HSET, и это легко не заметить —
обновление значения выглядит безобидно. Если срок нужен, задавайте его при каждой записи.
setAll()
Записывает несколько полей одной командой.
Записать пять полей одним вызовом — это один обмен с сервером вместо пяти. На горячем пути разница заметна.
Синтаксис
public function setAll(array $fields, ?int $ttl = null): boolПараметры
$fields — массив «поле → значение». Пустой массив ничего не делает.
$ttl — один срок жизни на все записываемые поля. По умолчанию null.
Возвращает
true, если команда выполнена; для пустого массива — тоже true, поскольку записать
нечего и это не ошибка.
Ошибки
LogicException — при $ttl меньше или равном нулю.
RedisFeatureException — если сервер не умеет HSETEX.
Пример
$cart->setAll([
'qty' => '2',
'sku' => 'A-1',
'total' => '1990',
]);
$session->setAll(['user' => $id, 'ip' => $ip], ttl: 3600); // срок у обоих полейincrement()
Атомарно увеличивает числовое поле.
Это одна команда на сервере, а не «прочитать, прибавить, записать»: два одновременных
запроса не могут оба прочитать 5 и оба записать 6.
Синтаксис
public function increment(string $field, int $by = 1): intПараметры
$field — имя поля. Если поля не было, оно создаётся со значением 0 и сразу
увеличивается.
$by — на сколько увеличить. По умолчанию 1.
Возвращает
Новое значение поля.
Ошибки
RedisCommandException — если в поле лежит не число:
$cart->set('sku', 'A-1');
$cart->increment('sku');RedisCommandException: ERR hash value is not an integerПример
$stats = $store->hash('stats:2026-08');
$stats->increment('16'); // 1 — первый просмотр за 16-е
$stats->increment('16', 5); // 6decrement()
Атомарно уменьшает числовое поле.
Во всём остальном идентичен increment(): та же атомарность, то же
создание отсутствующего поля из нуля, то же исключение на нечисловом значении.
Синтаксис
public function decrement(string $field, int $by = 1): intПараметры
$field — имя поля.
$by — на сколько уменьшить. По умолчанию 1.
Возвращает
Новое значение поля; уйти ниже нуля оно может.
Ошибки
RedisCommandException — если в поле лежит не число.
Пример
$stats->increment('16', 6); // 6
$stats->decrement('16'); // 5
$stats->decrement('16', 2); // 3delete()
Удаляет поля.
Синтаксис
public function delete(string ...$fields): intПараметры
...$fields — имена полей, сколько угодно. Можно не передать ни одного — тогда запрос не
отправляется.
Возвращает
Число полей, которые существовали и были удалены.
Пример
$cart->delete('promo'); // 1
$cart->delete('promo'); // 0 — его уже нет
$cart->delete('qty', 'sku', 'total'); // 3Хеш без полей перестаёт существовать
Redis не хранит пустые контейнеры: удаление последнего поля удаляет и ключ. Проверить
можно так же, как и наличие — count() === 0.
Сроки жизни полей
Отдельный срок у каждого поля — возможность новых версий Redis (см. ниже). Она пригодится там, где часть данных временная: блокировка внутри объекта, одноразовый код в сессии, кеш поверх записи.
ttl()
Сообщает, сколько полю осталось жить.
Синтаксис
public function ttl(string $field): ?intПараметры
$field — имя поля.
Возвращает
Оставшиеся секунды, либо null — и когда у поля нет срока, и когда нет самого поля.
Если различать эти случаи важно, спросите has().
Ошибки
RedisFeatureException — если сервер не умеет HTTL.
Пример
$cart->set('lock', '1', ttl: 60);
$cart->ttl('lock'); // 60
$cart->ttl('sku'); // null — у соседнего поля срока нет
$cart->ttl('nothing'); // null — и поля нетpersist()
Снимает срок с поля, оставляя само поле на месте.
Синтаксис
public function persist(string $field): boolПараметры
$field — имя поля.
Возвращает
true, если срок был снят; false, если снимать было нечего — у поля не было срока либо
поля нет.
Ошибки
RedisFeatureException — если сервер не умеет HPERSIST.
Пример
$session->set('code', $otp, ttl: 300);
$session->persist('code'); // true — код подтверждён, пусть остаётся
$session->persist('code'); // false — срока уже нетКлюч целиком
Методы этого раздела относятся к хешу как к одному ключу. Слово Key в названии стоит
именно поэтому: ttl('lock') — про поле, keyTtl() — про весь хеш.
Сроки ключа и полей независимы: у ключа может быть свой срок, у отдельных полей — свои, более короткие.
expireKey()
Задаёт срок жизни всему хешу.
Синтаксис
public function expireKey(int $seconds): boolПараметры
$seconds — через сколько секунд удалить хеш целиком. Отсчёт от текущего момента:
expireKey(time() + 86400) попросит пятьдесят шесть лет и не сообщит об этом.
Возвращает
true, если срок установлен; false, если ключа не существует.
Пример
$cart->setAll(['qty' => '1']);
$cart->expireKey(86400); // брошенная корзина живёт суткиkeyTtl()
Сообщает, сколько осталось жить всему хешу.
Синтаксис
public function keyTtl(): ?intПараметры
Нет.
Возвращает
Оставшиеся секунды, либо null — и когда срока нет, и когда нет ключа.
deleteKey()
Удаляет хеш со всеми полями.
Синтаксис
public function deleteKey(): boolПараметры
Нет.
Возвращает
true, если ключ существовал.
Пример
$cart->deleteKey(); // true
$cart->deleteKey(); // false — уже удалёнname()
Возвращает имя ключа с префиксом — то, что видит сервер.
Нужно для команд, которых нет в ручке: их выполняют через
raw(), а туда имя ключа надо передать полностью.
Синтаксис
public function name(): stringПараметры
Нет.
Возвращает
Полное имя ключа.
Пример
$cursor = null;
$store->raw()->hScan($cart->name(), $cursor, '*', 100); // обход большого хешаТребования к версии сервера
| Что | С какой версии |
|---|---|
| Всё, кроме сроков полей | любая |
ttl(), persist() |
Redis 7.4 (HTTL, HPERSIST) |
set() и setAll() с ttl |
Redis 8.0 (HSETEX) |
На более старом сервере эти методы бросают RedisFeatureException с точным объяснением —
какая команда, какая версия нужна, какая стоит и что делать вместо:
RedisFeatureException: Redis HSETEX (per-field lifetimes) needs server 8.0 or newer;
this server reports 7.2.4. Give the whole hash a lifetime with expireKey() instead.Почему отказ, а не запасной путь
На 7.4 можно было бы откатываться на HSET + HEXPIRE. Тогда поведение зависело бы от
версии сервера, а щель между двумя командами вернулась бы — и проявилась бы однажды в
проде, а не на глазах у разработчика. Один способ, который либо работает, либо честно
отказывается, надёжнее двух похожих.
Версия сервера запрашивается только в момент отказа, поэтому обычная работа за это сообщение ничем не платит.
Соответствие командам Redis
| Метод | Команда |
|---|---|
get() / getMany() / all() |
HGET / HMGET / HGETALL |
fields() / values() |
HKEYS / HVALS |
has() / count() |
HEXISTS / HLEN |
set() / setAll() |
HSET, со сроком — HSETEX |
increment() / decrement() |
HINCRBY |
delete() |
HDEL |
ttl() / persist() |
HTTL / HPERSIST |
expireKey() / keyTtl() / deleteKey() |
EXPIRE / TTL / DEL |
Чего здесь нет — HSCAN, HRANDFIELD, HINCRBYFLOAT, HSETNX — доступно через
raw() вместе с name().
Дальше
- Списки — очереди и блокирующее чтение
- Стримы — журнал событий и группы потребителей
- Сторы — строки, счётчики,
raw()и транзакции - Конфигурация — сериализация значений