Hash
Предпосылки: String, хеш-таблица.
Проблема overhead’а
При хранении объекта с полями отдельными STRING-ключами (user:123:name, user:123:age, user:123:email) каждый ключ несёт ~70 байт overhead: метаданные объекта, запись в глобальной хеш-таблице, запись в таблице TTL (если есть). Для объекта из 10 полей это ~700 байт служебных данных сверх полезной нагрузки. На миллионе пользователей разница между «10 STRING-ключей на профиль» и «1 HASH на профиль» — сотни мегабайт.
HASH группирует поля под одним ключом — один overhead на весь объект вместо десяти.
Один ключ, много полей
Профиль пользователя — набор полей, которые читаются и обновляются по отдельности или вместе. HASH хранит их под одним ключом:
HSET user:123 name "John" age 30 email "john@example.com"
HGET user:123 name -- → "John"
HMGET user:123 name age -- → ["John", "30"]
HGETALL user:123 -- → {name: "John", age: "30", email: "..."}
HEXISTS user:123 name -- → 1
HLEN user:123 -- → 3
HINCRBY user:123 login_count 1 -- атомарный инкремент числового поля
HDEL user:123 temporary_field -- удалить полеHSET принимает несколько пар поле-значение за один вызов — атомарное обновление без pipeline. HMGET возвращает только нужные поля, не загружая весь объект. HGETALL удобен для маленьких хешей, но на больших может стать проблемой.
Группировка полей экономит память, но появляется вопрос: что будет, когда хеш вырастет до тысяч полей?
Большие хеши и HSCAN
HGETALL на хеше с десятками тысяч полей возвращает мегабайты данных за одну команду. Пока Redis формирует ответ, однопоточный event loop не обрабатывает другие команды. На хеше из 10 000 полей это может занять несколько миллисекунд — незаметно для одного клиента, но блокирует все остальные.
HSCAN решает задачу итерации порциями. Как и SCAN для ключей, HSCAN использует курсор и возвращает набор полей за каждый вызов, не блокируя сервер:
HSCAN user:123 0 COUNT 100 -- первая порция (курсор 0 = начало)
-- → ["<cursor>", [field1, val1, field2, val2, ...]]Правило простое: если количество полей предсказуемо мало (профиль, настройки) — HGETALL безопасен. Если хеш может расти до тысяч полей — только HSCAN.
TTL полей
STRING-ключи истекают по отдельности: user:123:session живёт 30 минут, user:123:name — бессрочно. До версии 7.4 Redis позволял установить TTL только на весь ключ целиком — отдельные поля хеша не могли иметь собственный TTL.
Redis 7.4 добавил команды HEXPIRE, HPEXPIRE, HEXPIREAT и HPEXPIREAT, которые устанавливают TTL на отдельные поля хеша. Поле автоматически удаляется из хеша по истечении времени. Команды HTTL и HPTTL показывают оставшееся время жизни поля, а HPERSIST снимает TTL.
HSET user:123 name "John" session_token "abc" temp_data "xyz"
HEXPIRE user:123 1800 FIELDS 1 session_token -- session_token истечёт через 30 минут
HTTL user:123 FIELDS 1 session_token -- → 1800На версиях до 7.4 выбор ограничен: EXPIRE на весь ключ, отдельные STRING-ключи с индивидуальным TTL или Lua-скрипт, удаляющий поля по метке времени.
Две кодировки: listpack и hashtable
Маленькие хеши в Redis компактнее, чем можно ожидать. При малом количестве полей Redis использует listpack (src/listpack.c) — последовательный блок памяти, где поля и значения записаны друг за другом без указателей и заголовков. Поиск по полю — линейный обход (O(n)), но при нескольких десятках полей данные лежат в непрерывной памяти, и линейный обход оказывается быстрее хеш-таблицы с её разбросанными по памяти узлами. Хеш из 50 полей в listpack занимает примерно в 3 раза меньше памяти, чем в hashtable.
Когда количество полей превышает hash-max-listpack-entries (по умолчанию 512) или размер любого поля/значения превышает hash-max-listpack-value (по умолчанию 64 байта), Redis переключает кодировку на полноценную хеш-таблицу (src/dict.c) с O(1) доступом по полю. Переключение одностороннее: даже если потом удалить поля и хеш снова станет маленьким, обратной конвертации в listpack не произойдёт. Если удалить все поля — ключ удаляется автоматически: пустых хешей в Redis не бывает.
См. также
- Корзина checkout на HASH в Rails — HSET/HGETALL, обновление полей без гонки
Sources
- Redis Documentation: Hashes. https://redis.io/docs/data-types/hashes/
- Redis source:
src/listpack.c,src/dict.c,src/t_hash.c