Внутренние кодировки
Предпосылки: бит, байт, String (в том числе SDS), Hash, List, Set, Sorted Set.
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, STREAMencoding(4 бита) — внутренняя кодировка (int, embstr, raw, listpack, hashtable, quicklist, skiplist, intset и др.)lru(24 бита) — информация о последнем доступе для LRU/LFU evictionrefcount(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). Пороги переключения задаются конфигурацией:
| Тип | Порог по количеству | Порог по размеру |
|---|---|---|
| HASH | hash-max-listpack-entries | hash-max-listpack-value |
| ZSET | zset-max-listpack-entries | zset-max-listpack-value |
| LIST | — | list-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
- Redis Documentation: Memory optimization. https://redis.io/docs/management/optimization/memory-optimization/
- Redis source:
src/server.h(структураredisObject),src/listpack.c,src/intset.c