Valkey - не хранилище «ключ - значение» в смысле «положить блоб под именем». Каждый ключ содержит типизированную структуру, и команды работают с этой структурой на месте: увеличить поле hash, добавить элемент в конец list, добавить элемент в set, прочитать первую десятку sorted set. Выбор правильного типа - большая часть проектирования, потому что он решает, сколько обращений к серверу требует функция, атомарно ли обновление и сколько памяти занимают данные.
Коротко: strings - для отдельных значений и счётчиков, hashes - для объектов, lists - для очередей и последних элементов, sets - для принадлежности и уникальности, sorted sets - для всего, что ранжируется или упорядочено по времени, streams - для журналов событий. Bitmaps, HyperLogLog и геопространственные индексы очень дёшево закрывают три специализированные задачи. Остальная часть статьи разбирает каждый тип: важные команды, поведение памяти и ошибки, которые превращают быстрый сервер в медленный.
Как Valkey хранит ключ#
У каждого ключа есть имя, один тип и необязательный срок жизни. Запрос не того типа - это ошибка, а не преобразование:
SET page:views 10LPUSH page:views 1(error) WRONGTYPE Operation against a key holding the wrong kind of valueTYPE page:viewsstringСрок жизни принадлежит ключу целиком. До недавнего времени нельзя было задать срок жизни одному полю hash или одному элементу set; в Valkey 9.0 появился срок жизни отдельных полей hash (о нём ниже), но остальные типы по-прежнему истекают целиком. Об этом ограничении стоит знать заранее: если элементам нужны собственные сроки жизни, им обычно нужны собственные ключи или sorted set с оценкой по времени истечения.
Имена ключей - произвольные строки. Соглашение, которое делает пространство ключей управляемым, - сегменты через двоеточие от общего к частному, app:user:4182:profile, с сегментом версии там, где форма значения может измениться. Длинные имена ключей стоят памяти на каждом ключе, так что app:u:4182 оправдан, когда ключей десятки миллионов; для большинства приложений читаемость важнее.
Две команды показывают, чем ключ является на самом деле и сколько он стоит:
OBJECT ENCODING user:4182"listpack"MEMORY USAGE user:4182(integer) 120Кодировка важна, потому что маленькие коллекции хранятся в компактной форме - listpack или intset, - которая занимает лишь долю памяти общей формы. Valkey автоматически переводит коллекцию в общую форму, когда она пересекает порог размера. Подробнее - в разделе о памяти.
Strings и счётчики#
Базовый тип, несмотря на название, - это бинарно безопасная последовательность байт размером до 512 МБ. В ней хранят текст, JSON, сериализованные объекты, картинки, если очень хочется, а также целые числа и числа с плавающей точкой, над которыми сервер умеет выполнять арифметику.
| Команда | Что делает |
|---|---|
SET key value EX 300 NX | Записать с TTL 300 секунд, только если ключа нет |
GET key, MGET k1 k2 | Прочитать один или несколько |
INCR, INCRBY, INCRBYFLOAT | Атомарная арифметика |
GETDEL, GETEX | Прочитать и удалить или прочитать и изменить срок жизни |
APPEND, STRLEN, GETRANGE | Операции на уровне байт |
SET с NX и EX - основа простой блокировки и защиты идемпотентности: «дальше проходит только первый запрос с этим id». INCR - основа счётчиков и лимитов частоты (на нём построена статья ограничение частоты запросов на Valkey). Для кэширования string с сериализованным JSON - выбор по умолчанию и обычно правильный; TTL и инвалидация разобраны в статье шаблоны кэширования на Valkey.
Типичная ошибка - хранить большой JSON-документ как string, а затем переписывать его целиком ради одного поля. Если части объекта обновляются независимо, ему нужен hash.
Hashes для объектов#
Hash - это отображение полей на строковые значения под одним ключом, естественная форма для записи пользователя, сессии, товара, блока конфигурации.
HSET user:4182 name "Nino" plan "pro" logins 17HGET user:4182 planHINCRBY user:4182 logins 1HMGET user:4182 name planHGETALL user:4182HDEL user:4182 planКаждое поле можно читать и обновлять независимо, HINCRBY атомарен для каждого поля, а маленькие hashes хранятся очень компактно. Множество маленьких hashes - один из самых экономных по памяти способов держать объектные данные в Valkey.
Значения hash - плоские строки. Вложенные данные либо кладутся в одно поле как сериализованный JSON, либо разворачиваются в имена полей вроде address.city. Если вам нужны запросы внутрь вложенных документов, вы описываете документную базу данных, а не hash - см. статью какую базу данных выбрать.
В Valkey 9.0 появился срок жизни для отдельных полей hash: команды HSETEX, HGETEX, HEXPIRE, HTTL и HPERSIST задают, читают и снимают TTL одного поля. Это позволяет использовать один hash, например, для набора токенов по устройствам, каждый из которых истекает сам по себе. Прежде чем полагаться на это, проверьте версию сервера через INFO server (Valkey сообщает поле valkey_version рядом с redis_version, сохранённым для совместимости клиентов) и точный синтаксис в справочнике команд - старые серверы возвращают ошибку о неизвестной команде.
Lists для очередей и последних элементов#
List - это упорядоченная последовательность строк с быстрыми операциями на обоих концах.
LPUSH events:recent "login 4182"LTRIM events:recent 0 99LRANGE events:recent 0 9RPUSH jobs '{"task":"email","user":4182}'BLMOVE jobs jobs:processing LEFT RIGHT 5Добавление и снятие с любого конца выполняются за постоянное время. Чтение по индексу из середины длинного list - нет: LINDEX и LSET проходят по списку. Два типичных применения как раз под это подходят:
- Ограниченный список последних элементов.
LPUSH, а затемLTRIM 0 99хранит сто новейших элементов, за одно обращение при использовании pipeline. Ленты активности, журналы последних посещений, недавние поиски. - Простая очередь. Производители делают
RPUSH, потребители блокируются наBLPOPили, лучше, наBLMOVE, который атомарно переносит элемент в список обрабатываемых, чтобы упавший потребитель его не потерял. На этом фундаменте построены библиотеки вроде RQ; для всего серьёзного используйте библиотеку - они описаны в статье очереди задач на Valkey.
Не используйте list для проверок принадлежности («есть ли 4182 в этом списке?») - это линейный поиск. Для этого существуют sets.
Sets и sorted sets#
Set - неупорядоченная коллекция уникальных строк.
SADD online:users 4182 5530 7781SISMEMBER online:users 4182SCARD online:usersSINTER tags:valkey tags:tutorialSRANDMEMBER giveaway:entrants 3Добавление и проверка принадлежности выполняются за постоянное время, а дубликаты невозможны по построению. Применения: пользователи онлайн, теги, «голосовал ли уже этот пользователь», дедупликация при обходе сайтов, уникальные посетители, когда важна точность, а числа небольшие. SINTER, SUNION и SDIFF вычисляют операции над множествами на сервере, что лучше, чем тянуть два списка в приложение.
Sorted set даёт каждому уникальному элементу оценку с плавающей точкой и держит элементы упорядоченными по ней. Это самый универсальный тип в Valkey.
ZADD leaderboard 3120 "nino" 2875 "giorgi" 4010 "ana"ZINCRBY leaderboard 50 "giorgi"ZRANGE leaderboard 0 9 REV WITHSCORESZREVRANK leaderboard "nino"ZRANGE delayed 0 1791468000000 BYSCORE LIMIT 0 100ZREMRANGEBYSCORE sessions:active 0 1791460000000Добавление, удаление и поиск ранга выполняются за логарифмическое время; чтение диапазона - за логарифм плюс размер диапазона. Sorted sets подходят для:
- Таблиц лидеров и рейтингов, где
ZINCRBYиZREVRANK- ровно нужные операции. - Индексов по времени с временной меткой в качестве оценки: «задачи, срок которых наступил», «сессии, неактивные уже час», «посты за последние сутки».
- Отложенных задач и планирования - то же самое, увиденное с другой стороны.
- Лимитов частоты со скользящим окном, записывающих время каждого запроса.
- Вторичных индексов, которые вы поддерживаете сами: «товары по цене» как sorted set из id товаров с ценой в качестве оценки.
Помните о тяжёлых командах. SMEMBERS и ZRANGE 0 -1 на коллекции из миллиона элементов возвращают миллион элементов одним блокирующим вызовом; используйте SSCAN и ZSCAN или ограниченные диапазоны.
Streams, bitmaps, HyperLogLog и геоданные#
Streams - журнал только для добавления с id, группами потребителей и подтверждениями, надёжная альтернатива pub/sub. Тема достаточно глубокая для отдельной статьи: pub/sub и streams в Valkey.
Bitmaps - не отдельный тип, а команды для strings, которые обращаются со строкой как с массивом бит.
SETBIT active:2026-10-08 4182 1GETBIT active:2026-10-08 4182BITCOUNT active:2026-10-08BITOP AND active:both active:2026-10-07 active:2026-10-08Если смещения - числовые id пользователей, один бит на пользователя записывает «был активен сегодня». Миллион пользователей стоит около 125 КБ в день, а BITOP отвечает на вопрос «активен в оба дня», не трогая приложение. Подвох в том, что строка имеет длину, равную наибольшему смещению: один пользователь с id 4 000 000 000 выделяет 500 МБ. Используйте bitmaps только с плотными небольшими целочисленными id.
HyperLogLog оценивает число различных элементов в потоке значений, используя не больше примерно 12 КБ на ключ, со стандартной ошибкой 0,81 процента.
PFADD visitors:2026-10-08 "203.0.113.5" "198.51.100.7"PFCOUNT visitors:2026-10-08PFMERGE visitors:week visitors:2026-10-02 visitors:2026-10-08Уникальные посетители, уникальные поисковые запросы, различные устройства - везде, где достаточно приблизительного подсчёта, а хранить каждое значение было бы слишком дорого. Получить элементы обратно нельзя; структура только считает.
Геопространственные индексы - это sorted sets, в оценку которых закодированы координаты.
GEOADD stores 44.7930 41.7151 "tbilisi-1" 13.4050 52.5200 "berlin-1"GEOSEARCH stores FROMLONLAT 44.80 41.71 BYRADIUS 5 km ASC COUNT 5 WITHDISTGEODIST stores "tbilisi-1" "berlin-1" kmОбратите внимание на порядок: сначала долгота, потом широта. Перепутайте их - и ваш магазин в Тбилиси окажется в Индийском океане.
Кое-чего, что люди ожидают, в ядре Valkey нет: JSON-документы с запросами по пути, полнотекстовый поиск, фильтры Блума и временные ряды приходят из модулей, а у сервера без этих загруженных модулей таких команд нет. Не проектируйте в расчёте на них, пока не убедились, что они есть на сервере, которым вы будете пользоваться, - MODULE LIST показывает загруженное, если вашему пользователю разрешено её выполнять.
Память, кодировки и большие ключи#
Каждый ключ несёт накладные расходы - имя ключа, внутренний заголовок объекта, запись о сроке жизни, если он задан, - порядка десятков байт ещё до значения. Поэтому миллион крошечных ключей - совсем не крошечный набор данных. Группировка связанных мелких значений в один hash часто вдвое сокращает память, потому что маленький hash хранится как компактный listpack.
| Тип | Компактная форма | Пороги (по умолчанию) |
|---|---|---|
| Hash | listpack | до 128 полей, значения до 64 байт |
| Set из целых чисел | intset | до 512 элементов |
| Set | listpack | до 128 элементов, значения до 64 байт |
| Sorted set | listpack | до 128 элементов, значения до 64 байт |
| List | узлы listpack в quicklist | размер узла задаёт list-max-listpack-size |
Пороги - это параметры конфигурации (hash-max-listpack-entries, hash-max-listpack-value, set-max-intset-entries, set-max-listpack-entries, zset-max-listpack-entries и прочие). За ними структура переходит в общую форму, которая быстрее для больших коллекций и заметно больше расходует памяти на элемент. Менять значения по умолчанию приходится редко; знать же нужно, что OBJECT ENCODING объясняет, почему один hash в десять раз больше другого.
Более серьёзный эксплуатационный риск - большой ключ: одна коллекция с миллионами элементов или одна строка в сотни мегабайт. Удаление её через DEL блокирует сервер, пока он освобождает память (используйте UNLINK, который освобождает в фоне). Чтение целиком блокирует сервер и забивает сеть. Найти их можно так:
$ valkey-cli -h 203.0.113.20 -p 6380 --askpass --bigkeys$ valkey-cli -h 203.0.113.20 -p 6380 --askpass --memkeysОбе команды делают выборку пространства ключей через SCAN, так что на рабочем сервере они безопасны. Что происходит, когда память заканчивается, описано в статье память и политики вытеснения Valkey.
В RE:NODE тарифы Valkey бывают от 256 МБ до 4 ГБ памяти, и именно под это число нужно рассчитывать: всё описанное выше живёт в RAM, а на диске хранятся копии AOF и снимков.
Выбор типа: разобранный пример#
Небольшому сайту игрового сообщества нужны: профили игроков, еженедельная таблица лидеров, «кто сейчас онлайн», лента из последних пятидесяти событий, суточное число активных игроков и лимиты частоты в чате для каждого игрока.
| Функция | Тип | Ключ | Почему |
|---|---|---|---|
| Профиль | Hash | p:4182 | Поля обновляются независимо, компактно |
| Еженедельная таблица лидеров | Sorted set | lb:2026-w41 | ZINCRBY и ZRANGE ... REV, каждую неделю новый ключ с TTL |
| Сейчас онлайн | Sorted set | online | Оценка = время последней активности; ZREMRANGEBYSCORE убирает устаревших |
| Последние события | List | feed | LPUSH плюс LTRIM 0 49 |
| Активные за сутки | HyperLogLog | dau:2026-10-08 | 12 КБ при любом числе игроков |
| Лимит частоты в чате | Счётчик в string | rl:chat:4182:<minute> | INCR со сроком жизни |
Два решения здесь стоит отметить. «Сейчас онлайн» использует sorted set, а не set, потому что set не умеет убирать участников, переставших присылать heartbeat, а sorted set с оценкой по времени умеет - одной командой. А таблица лидеров каждую неделю получает новый ключ с TTL, так что таблица прошлой недели остаётся доступной для чтения, пока не истечёт, и никому не нужно сбрасывать очки в полночь воскресенья.
Обновление нескольких типов сразу
Настоящие функции затрагивают больше одного ключа. Когда игрок заканчивает матч, сайт увеличивает его очки в таблице лидеров, обновляет поле в профиле и добавляет событие в ленту. Отправить это можно тремя способами с разными гарантиями:
- Pipeline отправляет все три команды за одно сетевое обращение и читает ответы вместе. Это самый дешёвый вариант и правильный выбор по умолчанию, когда команды не зависят друг от друга. Он не атомарен: между ними может выполниться команда другого клиента.
- `MULTI` и `EXEC` ставят команды в очередь и выполняют их как один непрерываемый блок. Между ними ничего не выполняется. Отката, однако, нет: если одна команда упадёт, например с ошибкой
WRONGTYPE, остальные всё равно применятся. - Lua-скрипт выполняется атомарно и может ветвиться по прочитанному: «добавить в таблицу лидеров, только если id матча ещё не записан». Используйте его, когда решение зависит от текущих значений.
MULTIZINCRBY lb:2026-w41 120 "nino"HINCRBY p:4182 matches 1LPUSH feed "nino won on Dust"LTRIM feed 0 49EXECWATCH добавляет к MULTI оптимистичную блокировку: следите за ключом, прочитайте его, и если кто-нибудь изменит его до вашего EXEC, транзакция отбрасывается, и вы повторяете. Это работает, но при конкуренции короткий Lua-скрипт обычно проще. Для счётчиков и таблиц лидеров отдельные команды вроде INCR и ZINCRBY уже атомарны сами по себе, и это главная причина выбирать тип, в который нужная операция встроена.
FAQ#
Можно ли хранить JSON в Valkey?
Да, как string, и это обычный способ. Документ читается и записывается целиком. Если вы часто обновляете отдельные поля, используйте hash. Запросы по пути внутри JSON требуют модуля JSON, который не входит в ядро Valkey.
Каков максимальный размер значения?
String может занимать до 512 МБ, а коллекция в принципе может содержать миллиарды элементов. На практике держите значения маленькими: всё больше нескольких мегабайт медленно передаётся и блокирует сервер на время чтения или записи.
Hash или много ключей-strings?
Для объекта с несколькими полями - hash: накладные расходы одного ключа, компактная кодировка и атомарные обновления полей. Отдельные ключи-strings правильны, когда каждому значению нужен собственный TTL, на серверах без срока жизни полей hash.
Почему одна команда заблокировала весь сервер?
Команды выполняются по одной, так что дорогая команда - KEYS, SMEMBERS на огромном set, DEL на огромном ключе, ZRANGE 0 -1 на большом sorted set - заставляет ждать всех остальных клиентов. Используйте семейство SCAN и ограниченные диапазоны, а вместо DEL - UNLINK.
Занимают ли числа, хранимые как strings, больше памяти?
Значения, являющиеся целыми числами, хранятся в компактной целочисленной форме, так что SET counter 12345 стоит немногим больше самого ключа. Числа с плавающей точкой и числа с ведущими нулями хранятся как текст.




Комментарии
Полностью анонимно: без аккаунта, без почты, без cookie. Мы храним имя, которое вы ввели, текст и время - больше ничего. Количество ссылок ограничено, разметка не отображается.