Каждый тип данных Redis имеет несколько внутренних представлений: для маленьких значений — компактные упакованные структуры, для больших — быстрые классические. Понимание этого даёт реальную экономию памяти и предсказуемую производительность.
Тип и кодировка
У каждого значения есть тип (что видно команде TYPE: string, hash, list, set, zset, stream) и кодировка (как оно устроено внутри — OBJECT ENCODING). Redis сам переключает кодировку при росте коллекции. Кодировки — это ответ на вопрос «почему 10 маленьких хешей занимают 2 КБ, а 2 больших — 2 МБ».
String: int, embstr, raw
- int — значение целиком влезает в long:
SET a 42. Инкременты над таким значением бесплатны; - embstr — короткая строка (≤ 44 байта): заголовок объекта и данные лежат в одном аллокированном блоке — одна аллокация, лучшая локальность кеша CPU;
- raw — динамическая строка SDS. SDS хранит длину (strlen — O(1)), префикс длины позволяет безопасный аппенд, и она бинарно-безопасна (может содержать нулевые байты).
SET k1 12345 ; SET k2 "short" ; SET k3 "<длинная строка больше 44 байт....>"
OBJECT ENCODING k1 "int"
OBJECT ENCODING k2 "embstr"
OBJECT ENCODING k3 "raw"
APPEND k2 "-" # редкий случай: мутация embstr переводит его в raw
Hash: listpack → hashtable
Пока полей мало и значения короткие (пороги hash-max-listpack-entries 128,
hash-max-listpack-value 64), hash хранится как listpack — непрерывный буфер
«поле1, знач1, поле2, знач2…» с переменной длиной записей. Это экономно (нет указателей, нет
бакетов) и кэш-френдли, но чтение — линейный проход: нормально при десятках полей, плохо при
миллионе. Порог превышен → обычная хеш-таблица.
List: quicklist
Список — quicklist: двусвязный список узлов, где каждый узел — listpack на несколько сотен элементов. Компромисс: классический связный список тратит ~48+ байт указателей на элемент, один огромный listpack дорог при вставках в середину. LPUSH/RPOP — O(1) по краям, LRANGE по диапазону — эффективно.
Set: intset / listpack / hashtable
Сет только из целых чисел — intset: отсортированный массив целых (бинарный поиск).
Добавили строку или вышли за set-max-intset-entries → хеш-таблица. В Redis 7.2+
маленькие нечисловые сеты тоже живут в listpack.
ZSet: listpack → skiplist + dict
Отсортированный сет в большой кодировке держит две структуры одновременно:
- skip list — многоуровневый связный список, сортировка по score: O(log N) вставка, быстрые ZRANGEBYSCORE / ZRANK;
- dict member → score: O(1) ZSCORE.
Member уникален: повторный ZADD обновляет score. Равные score сортируются лексикографически — на этом построен лексикографический поиск ZRANGEBYLEX.
Stream: radix tree + listpack
Записи потока упакованы в listpack'и, а дерево по префиксам ID даёт быстрый поиск «по времени» и последовательное чтение. Отсюда дешёвый XADD в конец и эффективные диапазоны XRANGE.
Полная карта
| Тип | Малая кодировка | Большая кодировка | Порог (по умолчанию) |
|---|---|---|---|
| String | int / embstr | raw | > 44 байт |
| Hash | listpack | hashtable | 128 полей / 64 байта на значение |
| List | quicklist (узлы-listpack) | — | listpack-max-size −2 (≈8 КБ на узел) |
| Set | intset / listpack | hashtable | 128 элементов / 64 байта |
| ZSet | listpack | skiplist | 128 элементов / 64 байта |
| Stream | radix tree + listpack | — | |
Пороги настраиваются (CONFIG SET hash-max-listpack-entries ...), но менять их
без замеров не стоит: слишком агрессивный listpack замедляет чтение большого hash.
Практический вывод: как хранить объекты
💡 Один hash на объект почти всегда лучше N отдельных строковых ключей:HSET user:1001 name Ivan age 30вместоSET user:1001:name Ivan,SET user:1001:age 30. Экономия — на ключах, указателях и рехешинге; плюс атомарное чтение объекта целиком черезHGETALL(на маленьком hash — микросекунды).
JSON-строка против hash: строка проще и быстро читается целиком, но любое изменение —
перезапись и полный парс; hash позволяет обновить одно поле (HSET) и читать частями
(HMGET). Для «прочитал-кеш-объекта» — JSON нормален; для изменяемых сущностей — hash.
Практика
- Создайте hash и добавляйте по 50 полей с короткими значениями, после каждой порции смотрите
OBJECT ENCODING. Найдите точку переключения в hashtable. - Проверьте
OBJECT ENCODINGдля:SET n 3.14(float — это int? нет: raw/embstr, потому что не целое),SET b 00042(int? да, ведущие нули допустимы при парсинге), списка из 1000 элементов. - Сравните память:
MEMORY USAGEодного hash на 100 полей против 100 строковых ключей с теми же данными. - Посмотрите
DEBUG OBJECT key(выводит то же, но с полями refcount/serializedlength) иMEMORY DOCTOR.
Мини-квиз
1. В какой кодировке SET v "42"? — int: Redis парсит значение как
целое число.
2. Hash на 50 полей по 10 байт — какая кодировка и почему это хорошо? — listpack: нет накладных расходов на указатели/бакеты, кэш-френдли, чтение O(50) — микросекунды.
3. Почему ZSet держит и skiplist, и dict? — Skiplist — быстрые диапазоны по score, dict — быстрый ZSCORE по member.