Хеши
Хеш — словарь «поле → значение» под одним ключом. Это способ хранить объект целиком, не размазывая его по десятку ключей и не перечитывая целиком ради одного поля. Ниже — что это за структура, где она уместна, и разбор каждого метода.
Что такое хеш
Хеш — это отображение строк в строки внутри одного ключа 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()) и
меняют по одному.
Данные сессии. Поля читаются выборочно, а с Redis 8.0 отдельные поля могут ещё и жить своим сроком.
Где хеш — неправильный выбор:
| Задача | Почему не хеш | Что вместо |
|---|---|---|
| Найти объекты по значению поля | хеш не индексируется, придётся перебирать | вторичный индекс на множествах |
| Хранить вложенную структуру | значения — только строки | сериализация или отдельные ключи |
| Сортировать по значению | порядка нет | сортированное множество (ZSET) |
| Хранить очередь или историю | поля не упорядочены | список или стрим |
| Сотни тысяч полей с частым обходом | all() тянет всё разом |
несколько ключей или HSCAN |
Ручка
$cart = $store->hash('cart:42'); // сервер увидит 'session:cart:42'hash() ничего не выполняет — это представление, а не запрос. Префикс применяется
здесь один раз, поэтому дальше его негде забыть. Соединения ручка не держит: каждый
вызов спрашивает клиента у стора, поэтому её можно свободно передавать в пределах
запроса.
Все методы, кроме перечисленных в разделе «Ключ целиком», работают с полями. Это же
относится и к аргументу ttl: он задаёт срок тому, что пишет вызов, — то есть полю.
Справочник методов
Чтение
get()
public function get(string $field): mixedЗначение одного поля.
| Аргумент | Тип | По умолчанию | Что делает |
|---|---|---|---|
$field |
string |
— | имя поля |
Возвращает значение либо null — и если поля нет, и если нет самого хеша. Драйвер
в обоих случаях отдаёт false; null выбран, чтобы «нет значения» не путалось с
сохранённым false.
Бросает RedisCommandException, если ключ занят структурой другого типа — например,
списком. Это ошибка в коде, а не отсутствие данных, поэтому она не притворяется null.
$cart->get('qty'); // '2'
$cart->get('nothing'); // nullgetMany()
public function getMany(string ...$fields): arrayНесколько полей за один round-trip.
| Аргумент | Тип | По умолчанию | Что делает |
|---|---|---|---|
...$fields |
string |
— | имена полей; можно ни одного |
Возвращает массив «поле → значение», в котором всегда есть все запрошенные
имена: отсутствующие приходят как 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(); // []Это HGETALL: весь хеш одним ответом
Redis однопоточен, поэтому сборка ответа по хешу в сотни тысяч полей блокирует сервер
целиком, а результат ещё и оказывается в памяти процесса. Для таких хешей берите нужное
через getMany() или обходите пачками через HSCAN из
raw().
fields() и values()
public function fields(): array
public function values(): arrayТолько имена полей либо только значения. Аргументов нет. Возвращают массив; для несуществующего ключа — пустой.
Порядок не определён, но у обоих методов он одинаков — значения соответствуют именам позиция в позицию.
$cart->fields(); // ['qty', 'sku']
$cart->values(); // ['2', 'A-1']fields() дешевле all(), когда значения не нужны, — например, чтобы узнать, какие
дни есть в счётчике.
has()
public function has(string $field): bool| Аргумент | Тип | По умолчанию | Что делает |
|---|---|---|---|
$field |
string |
— | имя поля |
Возвращает true, если поле существует. Отличается от get() !== null тем, что не
передаёт значение по сети, — а для большого значения это заметно.
count()
public function count(): intЧисло полей. Аргументов нет. Возвращает количество, для несуществующего ключа —
0. Операция O(1).
if ($cart->count() === 0) {
return null; // корзины нет
}Запись
set()
public function set(string $field, mixed $value, ?int $ttl = null): boolЗаписывает одно поле, при необходимости — со сроком жизни этого поля.
| Аргумент | Тип | По умолчанию | Что делает |
|---|---|---|---|
$field |
string |
— | имя поля |
$value |
mixed |
— | значение. Строка или число; для остального нужен сериализатор |
$ttl |
?int |
null |
срок жизни поля в секундах от текущего момента. null — без срока |
Возвращает true, если команда выполнена. Бросает LogicException при ttl
меньше или равном нулю (это ошибка вызывающего: чтобы убрать поле, есть delete()) и
RedisFeatureException, если сервер старше 8.0 — см.
Требования к версии.
$cart->set('qty', '2');
$cart->set('lock', '1', ttl: 30); // это поле исчезнет через полминутыПерезапись без ttl снимает срок с поля
Проверено на живом сервере: поле со сроком в 100 секунд после set('x', '2') без ttl
теряет срок и остаётся навсегда. Так устроен HSET, и это легко не заметить —
обновление значения выглядит безобидно. Если срок нужен, задавайте его при каждой
записи.
С $ttl это одна команда HSETEX, а не запись с последующим HEXPIRE. Разница
существенная: между двумя командами есть щель, в которой поле уже есть, а срока у него
ещё нет, — и если в этот момент что-то сорвётся, поле останется навсегда.
setAll()
public function setAll(array $fields, ?int $ttl = null): boolЗаписывает несколько полей одной командой.
| Аргумент | Тип | По умолчанию | Что делает |
|---|---|---|---|
$fields |
array |
— | массив «поле → значение». Пустой массив ничего не делает |
$ttl |
?int |
null |
один срок жизни на все записываемые поля |
Возвращает true, если команда выполнена; для пустого массива — тоже true,
поскольку записать нечего и это не ошибка.
$cart->setAll([
'qty' => '2',
'sku' => 'A-1',
'total' => '1990',
]);
$session->setAll(['user' => $id, 'ip' => $ip], ttl: 3600); // срок у обоих полейЗаписать пять полей одним setAll() — это один round-trip вместо пяти. На горячем пути
разница заметна.
increment() и decrement()
public function increment(string $field, int $by = 1): int
public function decrement(string $field, int $by = 1): intАтомарно изменяют числовое поле.
| Аргумент | Тип | По умолчанию | Что делает |
|---|---|---|---|
$field |
string |
— | имя поля |
$by |
int |
1 |
на сколько изменить |
Возвращают новое значение поля. Если поля не было, оно создаётся со значением 0
и сразу изменяется. Бросают RedisCommandException, если в поле лежит не число.
$stats = $store->hash('stats:2026-08');
$stats->increment('16'); // 1 — первый просмотр за 16-е
$stats->increment('16', 5); // 6
$stats->decrement('16'); // 5Это одна команда на сервере, а не «прочитать, прибавить, записать»: два одновременных
запроса не могут оба прочитать 5 и оба записать 6.
$cart->set('sku', 'A-1');
$cart->increment('sku'); // RedisCommandException: hash value is not an integerРаньше такой вызов возвращал 0 — то есть отказ выглядел как «счётчик обнулился». Теперь
это исключение.
delete()
public function delete(string ...$fields): intУдаляет поля.
| Аргумент | Тип | По умолчанию | Что делает |
|---|---|---|---|
...$fields |
string |
— | имена полей; можно ни одного |
Возвращает число полей, которые существовали и были удалены. Без аргументов — 0,
запрос не отправляется.
$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 |
string |
— | имя поля |
Возвращает оставшиеся секунды либо null — и когда у поля нет срока, и когда нет
самого поля. Если различать эти случаи важно, спросите has().
$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 |
string |
— | имя поля |
Возвращает true, если срок был снят, и false, если снимать было нечего — у поля
не было срока либо поля нет.
$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 |
int |
— | через сколько секунд удалить хеш целиком |
Возвращает true, если срок установлен, и false, если ключа не существует.
$cart->setAll(['qty' => '1']);
$cart->expireKey(86400); // брошенная корзина живёт суткиСрок считается от текущего момента: expireKey(time() + 86400) попросит пятьдесят
шесть лет и не сообщит об этом.
keyTtl()
public function keyTtl(): ?intАргументов нет. Возвращает оставшиеся секунды жизни ключа либо null — и когда
срока нет, и когда нет ключа.
Сроки ключа и полей независимы: у ключа может быть свой срок, у отдельных полей — свои, более короткие.
deleteKey()
public function deleteKey(): boolУдаляет хеш со всеми полями. Аргументов нет. Возвращает true, если ключ
существовал.
name()
public function name(): stringИмя ключа с префиксом — то, что видит сервер. Нужно для команд, которых нет в ручке:
$cursor = null;
$store->raw()->hScan($cart->name(), $cursor, '*', 100); // обход большого хешаТребования к версии сервера
| Что | С какой версии |
|---|---|
| Всё, кроме сроков полей | любая |
ttl(), persist() |
Redis 7.4 (HTTL, HPERSIST) |
set(ttl:), 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()и транзакции - Конфигурация — сериализация значений