Программа курса

Модуль 01 / Урок 3 из 27

Структуры данных и их внутренние кодировки

Теория и практика

Каждый тип данных 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.

Полная карта

ТипМалая кодировкаБольшая кодировкаПорог (по умолчанию)
Stringint / embstrraw> 44 байт
Hashlistpackhashtable128 полей / 64 байта на значение
Listquicklist (узлы-listpack)—listpack-max-size −2 (≈8 КБ на узел)
Setintset / listpackhashtable128 элементов / 64 байта
ZSetlistpackskiplist128 элементов / 64 байта
Streamradix 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.

Практика

  1. Создайте hash и добавляйте по 50 полей с короткими значениями, после каждой порции смотрите OBJECT ENCODING. Найдите точку переключения в hashtable.
  2. Проверьте OBJECT ENCODING для: SET n 3.14 (float — это int? нет: raw/embstr, потому что не целое), SET b 00042 (int? да, ведущие нули допустимы при парсинге), списка из 1000 элементов.
  3. Сравните память: MEMORY USAGE одного hash на 100 полей против 100 строковых ключей с теми же данными.
  4. Посмотрите 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.