Внутренние кодировки

Предпосылки: бит, байт, String (в том числе SDS), Hash, List, Set, Sorted Set.

AOF | Eviction

INFO memory показывает 2 ГБ, хотя полезных данных — 400 МБ. Миллион мелких ключей, каждый с коротким значением. Куда делись полтора гигабайта? Два способа хранить тысячу пар ключ-значение: тысяча отдельных STRING-ключей или один HASH с тысячей полей. Интуитивно кажется, что разницы нет — данные одни и те же. На практике разница в потреблении памяти может достигать 5–10 раз.

Redis оборачивает каждое значение в служебную структуру и выбирает внутреннюю кодировку автоматически: для маленьких коллекций — компактную (listpack, intset), для больших — быструю с O(1) доступом (hashtable, skiplist). Пока коллекция укладывается в пороги компактной кодировки, она занимает в разы меньше памяти.

redisObject: ~16 байт на каждое значение

Каждое значение в Redis обёрнуто в структуру redisObject (src/server.h). В типичной 64-bit сборке она занимает ~16 байт:

  • type (4 бита) — тип данных: STRING, LIST, SET, ZSET, HASH, STREAM
  • encoding (4 бита) — внутренняя кодировка (int, embstr, raw, listpack, hashtable, quicklist, skiplist, intset и др.)
  • lru (24 бита) — информация о последнем доступе для LRU/LFU eviction
  • refcount (4 байта) — счётчик ссылок. Redis разделяет (shared objects) redisObject для часто встречающихся целых чисел 0–9999: вместо создания нового объекта используется ссылка на общий экземпляр
  • ptr (8 байт) — указатель на данные

Redis выбирает кодировку автоматически, исходя из содержимого и размера. Команда OBJECT ENCODING key показывает текущую кодировку.

Полный overhead на ключ

redisObject — только часть overhead’а. Каждый ключ в Redis — это запись в глобальной хеш-таблице (dict — основная хеш-таблица Redis, src/dict.c). Overhead складывается из: запись в хеш-таблице (dictEntry, в типичной 64-bit сборке порядка 24 байт: указатели на ключ, значение и следующий элемент), SDS-строка для имени ключа (16+ байт), redisObject для значения (~16 байт), запись в таблице expires (если есть TTL, ещё порядка 24 байт). Итого порядок величин — десятки байт на каждый ключ даже при коротких значениях. Это объясняет, почему миллион ключей может занять сотни мегабайт.

listpack: компактное представление для малых коллекций

Overhead на ключ неизбежен, но внутри коллекций (HASH, ZSET, LIST) Redis может хранить элементы компактно. Listpack (src/listpack.c) — последовательный блок памяти, где элементы записаны друг за другом без указателей. Каждый элемент хранит свою длину, что позволяет обходить listpack в обе стороны. Нет выделения памяти на каждый элемент — один блок на всю коллекцию.

Listpack используется как внутреннее представление для малых HASH, ZSET, и узлов quicklist (LIST). Пороги переключения задаются конфигурацией:

ТипПорог по количествуПорог по размеру
HASHhash-max-listpack-entrieshash-max-listpack-value
ZSETzset-max-listpack-entrieszset-max-listpack-value
LISTlist-max-listpack-size

При превышении порога Redis конвертирует HASH из listpack в hashtable, а ZSET — в skiplist + hashtable. Обратная конвертация обычно не происходит: если структура «выросла» до более тяжёлого представления, она остаётся в нём даже после удаления элементов.

intset: компактное множество целых чисел

Listpack подходит для коллекций из произвольных строк. Для множеств, состоящих только из целых чисел, Redis использует ещё более компактное представление. Intset (src/intset.c) — отсортированный массив целых чисел с адаптивным размером элемента (16/32/64 бита). Используется для SET, когда все элементы — целые числа и их количество не превышает set-max-intset-entries. При добавлении нечислового элемента или превышении порога конвертируется в hashtable.

Практические следствия

Понимание кодировок помогает проектировать экономные по памяти схемы. Если HASH укладывается в пороги listpack (hash-max-listpack-entries/hash-max-listpack-value), он хранится как listpack — один блок памяти без overhead на каждое поле. Когда пороги превышены, Redis переключается на hashtable, и потребление памяти обычно заметно вырастает. Это аргумент в пользу ограничения количества полей в хешах и использования коротких имён полей и значений.

Мониторинг кодировок: OBJECT ENCODING key показывает текущую кодировку. INFO memory показывает общее потребление памяти. MEMORY USAGE key (Redis 4.0+) показывает потребление конкретного ключа в байтах, включая overhead.

Практический пример: HASH хранит корзину покупок. Пока в ней до 512 товаров — listpack, ~8 КБ. Пользователь добавил 513-й товар — Redis переключил на hashtable, потребление выросло до ~30 КБ. На миллионе корзин такой переход увеличивает расход памяти на гигабайты. Параметры hash-max-listpack-entries и hash-max-listpack-value позволяют сдвинуть порог, но чем он выше, тем медленнее линейный поиск по listpack.

Когда кодировки уже оптимальны, но памяти не хватает, Redis применяет политики вытеснения — eviction.

Sources


AOF | Eviction