Проектирование ключей

Предпосылки: Внутренние кодировки, event loop.

Eviction | Репликация

Именование ключей

Тысяча ключей — порядок поддерживает здравый смысл. Миллион ключей без конвенции — хаос: session_abc, usr:42, temp, x. Непонятно, что можно удалить, что кому принадлежит, как найти все сессии. Redis не имеет таблиц и схем — все ключи живут в плоском пространстве имён внутри выбранной логической базы. Стандартная конвенция — разделение двоеточием: тип:идентификатор:поле.

user:123:profile         -- хеш с данными профиля
user:123:sessions        -- множество активных сессий
order:456:status         -- строка со статусом заказа
queue:emails             -- список-очередь
leaderboard:daily        -- sorted set

Двоеточие — конвенция, а не синтаксис: Redis не придаёт ему специального значения. Но инструменты визуализации (Redis Insight, RedisCommander) группируют ключи по префиксам с двоеточием, что помогает навигации.

Короткие имена экономят память. Каждый ключ хранится как SDS-строка, и при миллионах ключей разница между user:123:profile (16 байт) и u:123:p (7 байт) становится заметной. На практике компромисс — разумная читаемость без избыточной длины.

Поиск ключей по шаблону

Хорошее именование помогает структурировать данные, но иногда нужно найти ключи по шаблону — например, все сессии пользователя user:123:session:* или все очереди queue:*. Для этого Redis предоставляет команду KEYS pattern.

KEYS принимает glob-паттерн (как в shell): * — любая последовательность символов, ? — один символ, [ae] — один из перечисленных. KEYS user:* вернёт все ключи, начинающиеся с user:. Команда проходит по глобальной хеш-таблице, в которой Redis хранит все ключи, и проверяет каждый на соответствие паттерну. Вторичных индексов по именам нет, поэтому сложность всегда O(N) от общего числа ключей в базе, независимо от того, сколько из них совпадёт.

На миллионе ключей это блокирует event loop на десятки миллисекунд. На десяти миллионах — на сотни. Пока KEYS работает, все остальные клиенты ждут: Redis не может обработать ни одну команду, пока полное сканирование не завершится.

SCAN cursor [MATCH pattern] [COUNT hint] решает эту проблему итеративно. Redis возвращает порцию ключей и новый курсор. Между вызовами SCAN event loop обрабатывает другие команды. COUNT — подсказка о желаемом размере порции (по умолчанию 10), но Redis может вернуть больше или меньше.

SCAN 0 MATCH user:* COUNT 100
-- → ["cursor", ["user:1:profile", "user:2:profile", ...]]
-- повторять с новым курсором, пока курсор не станет "0"

Гарантии SCAN: все ключи, существовавшие от начала до конца итерации, будут возвращены. Ключи, добавленные или удалённые во время итерации, могут быть возвращены, а могут нет. Дубликаты возможны — клиент должен их фильтровать.

Аналогичные команды существуют для коллекций: HSCAN (поля хеша), SSCAN (элементы множества), ZSCAN (элементы sorted set).

Найти ключи — половина задачи, вторая — безопасно их удалить. DEL key удаляет ключ синхронно. Для маленьких значений (строка, маленький хеш) это мгновенно. Для большого списка из миллионов элементов или хеша с миллионом полей DEL блокирует event loop на время освобождения всей памяти — это могут быть сотни миллисекунд.

UNLINK key (Redis 4.0+) удаляет ключ из пространства имён синхронно (ключ сразу перестаёт быть доступен), но освобождение памяти передаёт в фоновый поток (lazyfree, src/lazyfree.c). Event loop не блокируется. Для маленьких объектов UNLINK ведёт себя как DEL (освобождение происходит сразу, без делегирования в фоновый поток, если объект достаточно мал).

Начиная с Redis 6.0 можно включить lazyfree-lazy-user-del yes — тогда DEL автоматически ведёт себя как UNLINK.

Инспекция ключей

INFO memory показывает общее потребление, но не отвечает на вопрос «какой ключ ест больше всего?» или «почему этот хеш вдруг стал тяжёлым?». Для диагностики конкретных ключей Redis предоставляет несколько команд.

OBJECT ENCODING mykey показывает текущую внутреннюю кодировку — listpack, hashtable, skiplist и т.д. Если хеш из десяти полей неожиданно занимает много памяти, OBJECT ENCODING покажет, что Redis уже переключился с компактного listpack на hashtable (потому что превышен hash-max-listpack-entries).

MEMORY USAGE mykey возвращает потребление в байтах, включая внутренний overhead Redis (SDS-заголовки, указатели, выравнивание). Это точнее, чем оценка по размеру данных.

OBJECT IDLETIME mykey и OBJECT FREQ mykey помогают понять поведение eviction. Первая команда показывает секунды с последнего доступа (при политике LRU), вторая — логарифмический счётчик частоты обращений (при LFU).

Автоматическое удаление пустых ключей

Для List, Set, Hash и Sorted Set Redis автоматически удаляет ключ, когда из коллекции удалён последний элемент. Не нужно проверять длину и вызывать DEL отдельно — после RPOP последнего элемента из списка или SREM последнего элемента из множества ключ перестаёт существовать: EXISTS key вернёт 0, а TTL key вернёт -2.

Stream ведёт себя иначе: даже если записей не осталось, ключ может продолжать существовать из-за метаданных (consumer groups хранят свои смещения). Для гарантированного удаления такого ключа подходят DEL или UNLINK.

Sources


Eviction | Репликация