Перейти к содержимому

Key-value (Ключ-значение)

Key-value базы данных — простейшая модель NoSQL с доступом по ключу за O(1). Основные операции: GET, SET, DELETE, INCR, EXPIRE. Популярные системы: Redis (in-memory, структуры данных: строки, хеши, списки, множества, sorted sets, pub/sub, Lua), Memcached (простой in-memory кеш без персистентности), RocksDB/LevelDB (встраиваемые, LSM-дерево, диск), DynamoDB (управляемая облачная). Паттерны использования: кеширование (cache-aside), сессии, счётчики, лимитирование, очереди, лидерборды, распределённые блокировки, геопространственные запросы. Проектирование ключей (префиксы, разделители). Сравнение с другими типами NoSQL, критерии выбора и типичные ошибки.

Введение: Самый простой и самый быстрый

Представьте себе шкаф с множеством ячеек. На каждой ячейке написан номер. Вы берете предмет, кладете в ячейку и запоминаете номер. Когда вам нужно найти предмет, вы идете прямо к этой ячейке. Не нужно перебирать все ячейки, не нужно искать по содержимому — вы знаете номер, и предмет находится мгновенно.

Это и есть модель “ключ-значение”. Самая простая и самая древняя структура данных в информатике. Словарь (dictionary), ассоциативный массив (associative array), хеш-таблица (hash table), карта (map) — все это разные названия одной и той же идеи.

Key-value база данных (хранилище “ключ-значение”) — это база данных, которая хранит данные как коллекцию пар “ключ-значение”. Ключ уникален и используется для доступа к значению. Значение — это произвольные данные (строка, число, JSON, бинарный объект), которые база данных не интерпретирует.

Это не просто “еще один тип NoSQL”. Это фундаментальная абстракция, на которой построены многие другие системы. Документо-ориентированные базы? Документ — это значение, а _id — это ключ. Колоночные базы? Первичный ключ — это ключ, а остальные колонки — значения. Кеширующие системы? Идеальный пример key-value.

Key-value хранилища — это максимальная простота и максимальная скорость. Никаких JOIN, никаких сложных запросов, никаких схем. Просто положить и получить.

Основные принципы

Ключ (Key)

Ключ — это уникальный идентификатор, по которому вы получаете доступ к данным. Ключ может быть:

Тип ключаПримерОсобенности
Целое число123, 100500Компактный, быстрый, но неинформативный
Строка"user:123", "session:abc123"Информативный, может содержать префиксы
БинарныйUUID, хешКомпактный, но нечитаемый
Составной"order:2024-01-01:123"Кодирует несколько параметров в ключе

Важные свойства ключей:

  • Уникальность: два значения не могут иметь одинаковый ключ
  • Простота сравнения: ключи должны быстро сравниваться
  • Равномерное распределение: для шардирования

Значение (Value)

Значение — это любые данные, которые вы хотите сохранить. Key-value база данных не интерпретирует содержимое значения. Для нее это просто последовательность байтов.

Тип значенияПримерКогда использовать
Строка"Hello, world!"Простые сообщения
Число42, 3.14159Счетчики, метрики
JSON{"name":"Иван","age":30}Структурированные данные
Бинарныйизображение, видеоФайлы, блобы
Сериализованный объектProtocol Buffer, MessagePack, PickleСложные структуры

Операции

Key-value хранилища поддерживают минимальный, но очень быстрый набор операций.

ОперацияОписаниеСложность
GET(key)Получить значение по ключуO(1)
SET(key, value)Записать значение по ключуO(1)
DELETE(key)Удалить пару ключ-значениеO(1)
EXISTS(key)Проверить существование ключаO(1)
INCR(key)Атомарно увеличить числовое значениеO(1)
EXPIRE(key, ttl)Установить время жизниO(1)

Примеры (Redis):

# Простейшие операции
SET user:123 "{\"name\":\"Иван\"}"
GET user:123
DEL user:123

# Работа со сроками жизни
SET session:abc123 "user_id=123" EX 3600   # Живет 1 час
EXPIRE temp:data 60                        # Умрет через 60 секунд
TTL session:abc123                         # Сколько осталось

# Атомарные операции со счетчиками
INCR page:home:views                       # Увеличить на 1
INCRBY page:home:views 100                 # Увеличить на 100
DECR inventory:123                         # Уменьшить на 1

Как это работает под капотом

Хеш-таблица (In-Memory)

Для in-memory key-value хранилищ (Redis, Memcached) используется хеш-таблица.

Хеш-таблица:

[0] → "user:123" → указатель на значение
[1] → NULL
[2] → "session:abc" → указатель на значение
[3] → "user:456" → указатель на значение
...
  • Время доступа: O(1) в среднем
  • Память: Данные хранятся в RAM
  • Долговечность: Требуется персистентность (снапшоты, AOF)

LSM-Tree (On-Disk)

Для дисковых key-value хранилищ (RocksDB, LevelDB, Badger) используется LSM-Tree (Log-Structured Merge-Tree).

Запись → MemTable (в памяти) → SSTable на диске → Компакция
  • Время доступа: O(log N) с амортизацией
  • Память: Основные данные на диске, кеш в памяти
  • Долговечность: Встроенная (WAL)

Распределенная хеш-таблица (DHT)

Для распределенных key-value хранилищ (DynamoDB, Cassandra в режиме KV, Riak) используется DHT.

    graph TD
    A[Ключ user:123] --> B[Хеш-функция]
    B --> C[Узел 1]
    B --> D[Узел 2]
    B --> E[Узел 3]
  
  • Масштабирование: Горизонтальное
  • Согласованность: Настраиваемая (eventual/strong)
  • Доступность: Высокая (репликация)

Популярные key-value базы данных

Redis (Remote Dictionary Server)

Самая популярная key-value БД. Работает в памяти, но может сохранять данные на диск.

# Redis — не только строки, но и структуры данных
SET user:123 "Иван"
HSET user:456 name "Петр" age 25           # Хеш
LPUSH queue:jobs "task1" "task2"           # Список
SADD tags:news "sport" "politics"          # Множество
ZADD leaderboard 100 "Иван" 200 "Петр"     # Сортированное множество

# Публикация/подписка
PUBLISH channel:news "Breaking news"
SUBSCRIBE channel:news

Характеристики:

  • In-memory (оперативная память)
  • Поддерживает сложные структуры данных (хеши, списки, множества, битовые карты)
  • Репликация, персистентность (RDB, AOF)
  • Lua-скриптинг
  • Pub/Sub, Streams

Когда использовать:

  • Кеширование
  • Сессии пользователей
  • Очереди (через списки)
  • Счетчики и метрики
  • Лимитирование (rate limiting)
  • Лидерборды (через sorted sets)

Когда НЕ использовать:

  • Большие объемы данных (дорого хранить в RAM)
  • Сложные запросы (нет индексов по значениям)
  • Долговременное хранение (есть риск потери при сбое)

Memcached

Простой in-memory кеш, без персистентности и сложных структур.

# Memcached — только строки
set user:123 0 3600 15
Иван Петрович
STORED

get user:123
VALUE user:123 0 15
Иван Петрович
END

Характеристики:

  • Только key-value (нет структур данных)
  • Нет персистентности (только кеш)
  • Максимальная производительность (проще Redis)
  • Распределенный хешинг (consistent hashing)

Когда использовать:

  • Простой кеш без сложных потребностей
  • Когда Redis избыточен

RocksDB / LevelDB

Встраиваемые key-value библиотеки на диске (LSM-Tree).

// RocksDB (C++)
rocksdb::DB* db;
rocksdb::DB::Open(options, "/path/to/db", &db);
db->Put(rocksdb::WriteOptions(), "key", "value");
db->Get(rocksdb::ReadOptions(), "key", &value);

Характеристики:

  • Встраиваемая (embedded) — работает внутри приложения
  • Дисковое хранилище (LSM-Tree)
  • Высокая производительность записи
  • Сжатие данных

Когда использовать:

  • База внутри приложения (не сервер)
  • Высокая нагрузка на запись
  • Ограниченная оперативная память

Amazon DynamoDB (режим Key-Value)

Управляемая облачная key-value БД.

// AWS SDK
await dynamodb.put({
    TableName: 'Users',
    Item: { userId: '123', name: 'Иван', age: 30 }
});

const result = await dynamodb.get({
    TableName: 'Users',
    Key: { userId: '123' }
});

Характеристики:

  • Полностью управляемый сервис AWS
  • Автоматическое масштабирование
  • Настраиваемая согласованность
  • Глобальные таблицы (мульти-регион)

Когда использовать:

  • Проекты на AWS
  • Нужно автоматическое масштабирование
  • Нет желания администрировать Redis

Riak / Riak KV

Распределенная AP-система (по CAP-теореме).

Характеристики:

  • Высокая доступность (AP)
  • Репликация, автоматическое восстановление
  • Ссылочные типы данных (CRDTs)

Когда использовать:

  • Высокая доступность важнее согласованности
  • Распределенная система без единой точки отказа

Продвинутые паттерны использования

Паттерн 1: Кеширование (Cache-Aside)

Самый распространенный паттерн. Приложение сначала проверяет кеш, при промахе — идет в основную БД.

async function getUser(userId) {
    // 1. Проверяем кеш (Redis)
    let user = await redis.get(`user:${userId}`);
    if (user) {
        return JSON.parse(user);
    }
    
    // 2. Промах кеша — идем в PostgreSQL
    user = await db.users.findOne({ id: userId });
    
    // 3. Сохраняем в кеш на 5 минут
    await redis.set(`user:${userId}`, JSON.stringify(user), 'EX', 300);
    
    return user;
}

Варианты:

  • Write-through: Запись сначала в кеш, потом в БД
  • Write-behind: Запись в кеш, асинхронно в БД
  • Cache-aside (lazy loading): Загрузка только при обращении

Паттерн 2: Счетчики и лимитирование

Атомарные операции инкремента идеальны для счетчиков.

// Счетчик просмотров страницы
async function recordPageView(pageId) {
    await redis.incr(`page:${pageId}:views`);
}

// Rate limiting (не более 100 запросов в минуту)
async function checkRateLimit(userId) {
    const key = `rate:${userId}:${Math.floor(Date.now() / 60000)}`;
    const count = await redis.incr(key);
    if (count === 1) {
        await redis.expire(key, 60);  // Живет 1 минуту
    }
    return count <= 100;
}

Паттерн 3: Сессии пользователей

Хранение сессий — классический use case для key-value.

// Сохранение сессии при логине
await redis.setex(
    `session:${sessionId}`,
    86400,  // 24 часа
    JSON.stringify({ userId: 123, role: 'admin', ip: '1.2.3.4' })
);

// Проверка сессии при каждом запросе
const session = await redis.get(`session:${sessionId}`);
if (!session) {
    return res.status(401).json({ error: 'Unauthorized' });
}

Паттерн 4: Очереди (Redis Lists)

// Producer
await redis.lpush('queue:emails', JSON.stringify({ to: 'user@example.com', subject: 'Hello' }));

// Consumer (блокирующий)
const task = await redis.brpop('queue:emails', 5);  // Ждет до 5 секунд
if (task) {
    processEmail(JSON.parse(task[1]));
}

Паттерн 5: Лидерборды (Redis Sorted Sets)

// Добавление очков
await redis.zincrby('leaderboard:game1', 100, 'user:123');
await redis.zincrby('leaderboard:game1', 50, 'user:456');

// Топ-10 игроков
const top10 = await redis.zrevrange('leaderboard:game1', 0, 9, 'WITHSCORES');

// Ранг игрока
const rank = await redis.zrevrank('leaderboard:game1', 'user:123');

Паттерн 6: Распределенные блокировки (Redis)

// Блокировка с таймаутом (чтобы не повисла)
const lockKey = `lock:resource:${resourceId}`;
const lockAcquired = await redis.set(lockKey, processId, 'NX', 'EX', 10);

if (lockAcquired) {
    try {
        // Критическая секция
        await doSomething();
    } finally {
        await redis.del(lockKey);
    }
} else {
    throw new Error('Resource locked');
}

Важно: Redis не гарантирует идеальные распределенные блокировки (есть проблема с часами и сетевыми задержками). Для критичных случаев используйте Redlock или ZooKeeper.

Паттерн 7: Геопространственные запросы (Redis)

# Добавление точек
GEOADD locations 37.617 55.752 "Moscow"
GEOADD locations -73.935 40.730 "New York"

# Поиск в радиусе 1000 км от Москвы
GEORADIUS locations 37.617 55.752 1000 km

# Расстояние между городами
GEODIST locations Moscow "New York" km

Плюсы и минусы key-value хранилищ

Плюсы

ПлюсОписание
Максимальная производительностьO(1) доступ, микросекундные задержки
ПростотаНет схем, нет сложных запросов
Горизонтальное масштабированиеОтлично шардируется по ключу
Низкая задержкаОсобенно у in-memory решений
Высокая доступностьЛегко реплицируется

Минусы

МинусОписание
Поиск только по ключуНельзя искать по значению или по части ключа
Нет связейНет JOIN, нет ссылочной целостности
Ограниченные запросыНет агрегаций, группировок, оконных функций
Ограниченная память (in-memory)RAM дороже диска
Eventual consistencyНекоторые системы не гарантируют сильную согласованность

Key-value в сравнении с другими типами NoSQL

ХарактеристикаKey-valueДокументнаяКолоночнаяГрафовая
Модель данныхПары ключ-значениеJSON-документыКолонкиВершины и ребра
Поиск по значениюНет (только по ключу)ДаДаДа
Сложные запросыНетОграниченноДа (аналитика)Да (обход графа)
Вложенные данныеНет (но value может быть JSON)ДаНетДа
СвязиНетОграниченно ($lookup)НетДа (основная модель)
ПроизводительностьМаксимальнаяВысокаяВысокая (аналитика)Средняя
Типичное применениеКеш, сессииКаталоги, CMSАналитикаСоцсети, рекомендации

Когда выбирать key-value

Идеальные сценарии

СценарийПочему подходит
КешированиеБыстрый доступ, TTL, не требуется постоянное хранение
Сессии пользователейДоступ по session_id, короткое время жизни
Счетчики и метрикиАтомарные операции инкремента
Очереди задачLPUSH/RPOP, блокирующие операции
Лимитирование (rate limiting)Окна времени, счетчики
ЛидербордыСортированные множества
Распределенные блокировкиSET NX EX
Pub/SubЛегковесные уведомления

Сомнительные сценарии

СценарийПочему плохо подходит
Сложные запросы (WHERE age > 18 AND city = ‘Москва’)Нет индексов по значениям
Связи между объектами (JOIN)Нет реляционной модели
Аналитика и отчетыНет агрегаций
Большие объемы данныхIn-memory дорого, дисковые KV медленнее
Долговременное хранение критичных данныхНекоторые KV (Memcached) не имеют персистентности

Выбор key-value базы данных: Сравнение

КритерийRedisMemcachedRocksDBDynamoDB
ХранениеIn-memory + disk (опционально)In-memoryDisk (LSM)SSD (управляемый)
Структуры данныхСтроки, хеши, списки, множества, ZSET, битмапы, геоТолько строкиСтроки, колонкиДокументы, ключ-значение
ПерсистентностьRDB, AOFНетДа (на диске)Да (облачная)
РепликацияMaster-replica, Sentinel, ClusterНетВстраиваемаяАвтоматическая (глобальные таблицы)
ТранзакцииLua-скрипты (атомарно), MULTI/EXECНетДа (ACID на уровне одной операции)Транзакции на нескольких элементах
МасштабированиеCluster (шардирование)Consistent hashingВстраиваемаяАвтоматическое
Сложность администрированияСредняяНизкаяНизкая (embedded)Нулевая (управляемая)
ЦенаБесплатно (open source)БесплатноБесплатноПлатно (по запросам)

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

В key-value хранилищах проектирование ключей — это искусство. Поскольку нельзя искать по значению, всю логику запроса нужно закодировать в ключе.

Стратегии именования ключей

Использование префиксов:

user:123
user:456:profile
session:abc123
order:2024-01-01:123

Использование составных ключей (с разделителем):

"user:123:profile"
"user:123:settings"
"user:123:orders:2024-01"

Использование хешей для группировки (Redis):

HSET user:123 name "Иван" email "ivan@example.com" age 30
HGET user:123 name
HGETALL user:123

Шаблоны хороших ключей

ШаблонПримерПочему хорошо
entity:iduser:123Просто, понятно, легко удалить группу через SCAN user:*
entity:id:attributeuser:123:emailПозволяет обновлять отдельные атрибуты
entity:id:range:dateorder:2024-01:123Позволяет сканировать диапазоны (с осторожностью)
namespace:entity:idapp1:user:123Изоляция между приложениями/арендаторами

Чего избегать в ключах

АнтипаттернПочему плохоПример
Слишком длинные ключиЗанимают память, замедляют сравнение"very_long_namespace_for_user_profile_information_123"
Случайные ключиПлохое распределение в DHTUUID v4 в качестве ключа
Ключи без префиксовНельзя найти все записи одного типа"123" (что это? user? order?)
Ключи с пробелами и спецсимволамиПроблемы с экранированием"user:Иван Иванов"

Распространенные ошибки

Ошибка 1: Использование Redis как единственной БД

Redis — это не замена PostgreSQL. Он не поддерживает сложные запросы, не гарантирует долговечность (в режиме по умолчанию), не умеет в JOIN.

Как исправить: Используйте Redis как кеш или для специфических задач, но основное хранилище — реляционная или документная БД.

Ошибка 2: Хранение больших значений в Redis

Redis загружает все данные в память. Хранение гигабайтных значений (видео, изображений) разорительно.

Как исправить: Для больших объектов используйте S3, MinIO, Blob Storage. В Redis храните только ссылки (URL, ключи).

Ошибка 3: Игнорирование TTL (времени жизни)

Данные в кеше накапливаются, память заканчивается, сервер падает.

Как исправить: Всегда устанавливайте TTL для кешируемых данных. Используйте политики вытеснения (LRU, LFU).

Ошибка 4: Использование KEYS в Redis (продакшн)

# Опасно! Блокирует сервер на время сканирования всех ключей
KEYS user:*

# Безопасно
SCAN 0 MATCH user:* COUNT 100

Ошибка 5: Наивные распределенные блокировки

// Плохо (нет таймаута)
await redis.set('lock:resource', processId);
// ... если процесс упадет, блокировка останется навсегда

Как исправить: Используйте SET NX EX или Redlock.

Резюме для системного аналитика

  1. Key-value хранилища — самые простые и самые быстрые среди NoSQL. Доступ по ключу за O(1), никаких сложных запросов, никаких схем. Идеальны для кеширования, сессий, счетчиков, очередей.

  2. Главное ограничение — только доступ по ключу. Нельзя искать по значению, нельзя делать JOIN, нельзя выполнять сложные агрегации. Если вам нужен поиск по полям — выбирайте документную или реляционную БД.

  3. Redis — король key-value. In-memory, богатые структуры данных, Lua-скриптинг, репликация, персистентность. Memcached — проще, быстрее, но без структур данных и персистентности.

  4. Встраиваемые KV (RocksDB, LevelDB) — для приложений, которым нужна локальная база. Высокая производительность записи, дисковое хранение, работает внутри вашего процесса.

  5. Две основные модели персистентности: in-memory (Redis, Memcached) — максимальная скорость, но данные в RAM; on-disk (RocksDB) — больше места, но медленнее.

  6. Проектирование ключей — ключевой навык. Хороший ключ должен быть коротким, информативным, равномерно распределенным. Используйте префиксы и разделители.

Проверка знаний

Вопрос 1 из 4
Что такое key-value база?
Когда key-value модель особенно эффективна?
Какое ограничение типично для key-value БД?
Почему Redis часто приводят как пример key-value БД?

Вопросы, где были ошибки