KV Store
KV Store — это распределённое хранилище ключ-значение (key-value), доступное для ваших приложений через Runtime API. Используйте его для хранения данных сессий, кэша, конфигураций и других небольших данных.
Что такое KV Store
Заголовок раздела «Что такое KV Store»KV Store предоставляет:
- Низкую задержку — данные хранятся на edge, рядом с вашим приложением
- Глобальную доступность — данные доступны из любого региона
- Простой API — базовые операции: get, set, delete, list, has
- Изоляцию по окружениям — каждое окружение имеет свой KV Store
Когда использовать KV Store
Заголовок раздела «Когда использовать KV Store»| Сценарий | Пример |
|---|---|
| Сессии пользователей | Хранение JWT токенов или сессионных данных |
| Кэш | Кэширование API ответов, страниц |
| Конфигурация | Feature flags, настройки приложения |
| Rate limiting | Счётчики запросов по IP |
| A/B тестирование | Назначение пользователей в группы |
Управление через UI
Заголовок раздела «Управление через UI»Просмотр и редактирование (KV Browser)
Заголовок раздела «Просмотр и редактирование (KV Browser)»-
Откройте страницу окружения
Перейдите в проект → выберите окружение → вкладка KV Store
-
Просмотр записей
Все записи отображаются в таблице с ключом, значением и TTL (время жизни)
-
Добавление записи
Нажмите Add Entry и укажите:
- Key — уникальный ключ
- Value — значение (текст или JSON)
- TTL — время жизни в секундах (опционально)
-
Редактирование
Нажмите на запись для изменения значения или TTL
-
Удаление
Нажмите иконку удаления рядом с записью для удаления
Поиск и фильтрация
Заголовок раздела «Поиск и фильтрация»- Используйте поле поиска для фильтрации по ключу
- Поддерживается префиксный поиск (например,
session:найдёт все ключи начинающиеся сsession:)
Использование в приложении
Заголовок раздела «Использование в приложении»Установите SDK и импортируйте модуль KV:
npm install @onreza/runtime# или: pnpm add / yarn add / bun addimport { kv, isKVAvailable } from '@onreza/runtime/kv';
// Получение значенияconst value = await kv.get('my-key');
// Получение JSONconst data = await kv.get('config', { type: 'json' });
// Установка значения с TTL (в секундах)await kv.set('session:123', { user: 'alice' }, { ttl: 86400 });
// Проверка существования ключаconst exists = await kv.has('my-key');
// Список ключей по префиксу с пагинациейconst { keys, cursor } = await kv.list({ prefix: 'session:', limit: 100 });
// Удалениеawait kv.delete('my-key');
// Получение нескольких ключей за один запросconst [user, profile] = await kv.getMany( ['user:123', 'profile:123'], { type: 'json' });
// Получение значения вместе с метаданнымиconst { value, metadata } = await kv.getWithMetadata('cache:user:123', { type: 'json' });→ Полный API reference: Runtime SDK
Пример в Compute App (Next.js)
Заголовок раздела «Пример в Compute App (Next.js)»KV Store работает в Compute Apps с тем же API:
// app/api/session/route.ts (Next.js App Router)import { kv } from '@onreza/runtime/kv';
export async function GET(request: Request) { const token = request.headers.get('authorization')?.replace('Bearer ', ''); if (!token) return new Response('Unauthorized', { status: 401 });
// Работает и в Edge Functions, и в Compute Apps const session = await kv.get(`session:${token}`, { type: 'json' }); if (!session) return new Response('Session expired', { status: 401 });
return Response.json(session);}Binary Data
Заголовок раздела «Binary Data»KV Store поддерживает хранение бинарных данных через Uint8Array:
// Запись бинарных данныхconst binaryData = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]); // "Hello"
await kv.set('binary-key', binaryData);
// Чтение бинарных данныхconst value = await kv.get('binary-key');// value: Uint8Array [72, 101, 108, 108, 111]Пример: хранение изображения
Заголовок раздела «Пример: хранение изображения»// Сохранение изображения в KV Storeasync function storeImage(key, imageBuffer) { // imageBuffer — Uint8Array или Buffer await kv.set(key, new Uint8Array(imageBuffer));}
// Получение изображенияasync function getImage(key) { const data = await kv.get(key); if (data instanceof Uint8Array) { return data; // бинарные данные } return null;}
// Использованиеawait storeImage('avatar:user123', imageBuffer);const avatar = await getImage('avatar:user123');Тарифные лимиты
Заголовок раздела «Тарифные лимиты»| Метрика | Hobby | Pro |
|---|---|---|
| Storage | Недоступен | 2 GB |
Стоимость сверх лимита (Pro): ₽20/ГБ. Подробнее: Лимиты и квоты.
Технические лимиты
Заголовок раздела «Технические лимиты»| Параметр | Лимит |
|---|---|
| Размер ключа | 512 байт |
| Размер значения | 1 MB (включая бинарные данные) |
| TTL минимум | 60 секунд |
| TTL максимум | 1 год |
| Ключей в batch (getMany) | 100 |
| Ключей в list | 1000 за запрос |
Лучшие практики
Заголовок раздела «Лучшие практики»Структура ключей
Заголовок раздела «Структура ключей»Используйте префиксы для организации данных:
session:<user-id> # Сессии пользователейcache:<api-endpoint> # Кэш APIconfig:feature-flags # Конфигурацияrate:<ip-address> # Rate limitingblob:<file-id> # Бинарные данныеTTL (Time To Live)
Заголовок раздела «TTL (Time To Live)»Устанавливайте TTL для временных данных:
- Сессии — 24 часа
- Кэш — 5-15 минут
- Rate limiting — 1 минута
// Сессия на 24 часаawait kv.set(`session:${userId}`, sessionData, { ttl: 86400 });
// Кэш на 5 минутawait kv.set(`cache:${url}`, response, { ttl: 300 });Обработка ошибок
Заголовок раздела «Обработка ошибок»import { kv } from '@onreza/runtime/kv';
try { const value = await kv.get('my-key'); if (value === null) { // Ключ не найден return defaultValue; } return value;} catch (error) { // Fallback при недоступности KV console.error('KV error:', error); return defaultValue;}Паттерны кэширования
Заголовок раздела «Паттерны кэширования»Cache-Aside (Lazy Loading)
Заголовок раздела «Cache-Aside (Lazy Loading)»Самый распространённый паттерн — проверить кэш, при промахе загрузить данные и сохранить:
import { kv } from '@onreza/runtime/kv';
async function getCachedData<T>(key: string, fetcher: () => Promise<T>, ttl = 300): Promise<T> { // 1. Проверяем кэш const cached = await kv.get(key, { type: 'json' }); if (cached !== null) return cached as T;
// 2. Загружаем данные const data = await fetcher();
// 3. Сохраняем в кэш await kv.set(key, data, { ttl }); return data;}
// Использованиеconst user = await getCachedData( `cache:user:${userId}`, () => fetchUserFromAPI(userId), 600 // 10 минут);Write-Through
Заголовок раздела «Write-Through»При каждой мутации обновляем и источник данных, и кэш одновременно:
import { kv } from '@onreza/runtime/kv';
async function updateUserProfile(userId: string, data: { name: string }) { // 1. Записываем в источник данных (БД, внешний API и т.д.) await fetch(`https://api.example.com/users/${userId}`, { method: 'PATCH', body: JSON.stringify(data), });
// 2. Сразу обновляем кэш — читатели получат свежие данные await kv.set(`cache:user:${userId}`, data, { ttl: 600 });}Stale-While-Revalidate
Заголовок раздела «Stale-While-Revalidate»Отдаём устаревшие данные мгновенно, обновляем кэш в фоне:
import { kv } from '@onreza/runtime/kv';
async function getWithSWR<T>(key: string, fetcher: () => Promise<T>, ttl = 300): Promise<T> { const cached = await kv.get(key, { type: 'json' }) as T | null;
// Фоновое обновление (не ждём результата) const refreshPromise = fetcher().then(data => kv.set(key, data, { ttl }) ).catch(() => {}); // игнорируем ошибки фонового обновления
if (cached !== null) return cached;
// Кэш пуст — ждём загрузки const data = await fetcher(); await kv.set(key, data, { ttl }); return data;}Инвалидация по префиксу
Заголовок раздела «Инвалидация по префиксу»Удаление группы связанных ключей:
import { kv } from '@onreza/runtime/kv';
async function invalidateByPrefix(prefix: string) { let cursor: string | undefined;
do { const result = await kv.list({ prefix, limit: 100, cursor }); await Promise.all(result.keys.map(key => kv.delete(key))); cursor = result.cursor; } while (cursor);}
// Удалить весь кэш пользователяawait invalidateByPrefix(`cache:user:${userId}`);Graceful Degradation
Заголовок раздела «Graceful Degradation»Если KV недоступен, приложение продолжает работать:
import { kv, isKVAvailable } from '@onreza/runtime/kv';
async function getData(key: string, fetcher: () => Promise<unknown>) { if (isKVAvailable()) { try { const cached = await kv.get(key, { type: 'json' }); if (cached !== null) return cached; } catch { // KV недоступен — загружаем напрямую } }
const data = await fetcher();
// Попытка сохранить в кэш (не критично при ошибке) if (isKVAvailable()) { kv.set(key, data, { ttl: 300 }).catch(() => {}); }
return data;}Практические примеры
Заголовок раздела «Практические примеры»Управление сессиями (Next.js App Router)
Заголовок раздела «Управление сессиями (Next.js App Router)»Полный паттерн: создание, проверка, продление и завершение сессии через KV.
import { kv } from '@onreza/runtime/kv';import { cookies } from 'next/headers';
const SESSION_TTL = 86400; // 24 часаconst SESSION_COOKIE = 'session_id';
interface Session { userId: string; email: string; role: string;}
export async function createSession(data: Session): Promise<string> { const sessionId = crypto.randomUUID(); await kv.set(`session:${sessionId}`, data, { ttl: SESSION_TTL }); return sessionId;}
export async function getSession(): Promise<Session | null> { const sessionId = (await cookies()).get(SESSION_COOKIE)?.value; if (!sessionId) return null;
const session = await kv.get(`session:${sessionId}`, { type: 'json' }) as Session | null;
// Продлеваем TTL при каждом запросе if (session) { await kv.set(`session:${sessionId}`, session, { ttl: SESSION_TTL }); }
return session;}
export async function destroySession(): Promise<void> { const sessionId = (await cookies()).get(SESSION_COOKIE)?.value; if (sessionId) await kv.delete(`session:${sessionId}`);}import { createSession } from '@/lib/session';import { cookies } from 'next/headers';
export async function POST(request: Request) { const { email, password } = await request.json();
// Проверка учётных данных (ваша логика) const user = await verifyCredentials(email, password); if (!user) return Response.json({ error: 'Invalid credentials' }, { status: 401 });
const sessionId = await createSession({ userId: user.id, email: user.email, role: user.role, });
(await cookies()).set('session_id', sessionId, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 86400, });
return Response.json({ ok: true });}import { getSession } from '@/lib/session';
export async function GET() { const session = await getSession(); if (!session) return Response.json({ error: 'Unauthorized' }, { status: 401 });
return Response.json({ userId: session.userId, role: session.role });}Кэш запросов Prisma
Заголовок раздела «Кэш запросов Prisma»Cache-aside с инвалидацией и getMany для batch-загрузки.
import { kv } from '@onreza/runtime/kv';import { prisma } from '@onreza/database';
const USER_TTL = 3600; // 1 час
export async function getUserById(id: string) { const cacheKey = `cache:user:${id}`;
// Проверяем кэш const cached = await kv.get(cacheKey, { type: 'json' }); if (cached) return cached;
// Промах — запрашиваем из БД const user = await prisma.user.findUnique({ where: { id } }); if (!user) return null;
await kv.set(cacheKey, user, { ttl: USER_TTL }); return user;}
// Batch-загрузка нескольких пользователейexport async function getUsersByIds(ids: string[]) { const cacheKeys = ids.map(id => `cache:user:${id}`); const cached = await kv.getMany(cacheKeys, { type: 'json' });
// Находим отсутствующие в кэше const missing = ids.filter((_, i) => cached[i] === null);
if (missing.length > 0) { const dbUsers = await prisma.user.findMany({ where: { id: { in: missing } }, });
// Сохраняем в кэш параллельно await Promise.all( dbUsers.map(user => kv.set(`cache:user:${user.id}`, user, { ttl: USER_TTL }) ) );
// Подставляем в результат for (const user of dbUsers) { const i = ids.indexOf(user.id); if (i !== -1) cached[i] = user; } }
return cached;}
// Инвалидация при обновленииexport async function updateUser(id: string, data: { name?: string; email?: string }) { const user = await prisma.user.update({ where: { id }, data }); await kv.delete(`cache:user:${id}`); return user;}Rate limiting
Заголовок раздела «Rate limiting»Ограничение числа запросов на пользователя через KV-счётчик.
import { kv } from '@onreza/runtime/kv';
interface RateLimitResult { allowed: boolean; remaining: number; resetIn: number; // секунды до сброса}
export async function rateLimit( identifier: string, limit = 60, windowSec = 60): Promise<RateLimitResult> { const key = `ratelimit:${identifier}`; const counter = await kv.get(key, { type: 'json' }) as | { count: number; expiresAt: number } | null;
const now = Math.floor(Date.now() / 1000);
if (!counter || counter.expiresAt <= now) { // Первый запрос в окне await kv.set(key, { count: 1, expiresAt: now + windowSec }, { ttl: windowSec }); return { allowed: true, remaining: limit - 1, resetIn: windowSec }; }
if (counter.count >= limit) { return { allowed: false, remaining: 0, resetIn: counter.expiresAt - now }; }
await kv.set( key, { count: counter.count + 1, expiresAt: counter.expiresAt }, { ttl: counter.expiresAt - now } );
return { allowed: true, remaining: limit - counter.count - 1, resetIn: counter.expiresAt - now, };}// middleware.ts (Next.js)import { rateLimit } from '@/lib/rate-limit';
export async function middleware(request: Request) { const ip = request.headers.get('x-forwarded-for') ?? 'unknown'; const { allowed, remaining, resetIn } = await rateLimit(`ip:${ip}`, 60, 60);
if (!allowed) { return new Response('Too Many Requests', { status: 429, headers: { 'Retry-After': String(resetIn) }, }); }
const response = await fetch(request); response.headers.set('X-RateLimit-Remaining', String(remaining)); return response;}Сравнение с Environment Variables
Заголовок раздела «Сравнение с Environment Variables»| Environment Variables | KV Store | |
|---|---|---|
| Назначение | Конфигурация, секреты | Динамические данные |
| Изменение | Требует redeploy | Мгновенно доступно |
| Доступ | Чтение только в приложении | Чтение/запись в runtime |
| Объём | До 4 KB на значение | До 1 MB на значение |
| Количество | До 200 переменных | До 100 000 записей (Pro) |
| Бинарные данные | Не поддерживается | Поддерживается (Uint8Array) |
Troubleshooting
Заголовок раздела «Troubleshooting»”KV Store unavailable”
Заголовок раздела «”KV Store unavailable”»- Проверьте что окружение существует
- Убедитесь что вы используете план Pro
- Проверьте что код выполняется внутри Edge Function или Compute App
”Rate limit exceeded”
Заголовок раздела «”Rate limit exceeded”»- Вы превысили лимит запросов
- Добавьте кэширование на уровне приложения
”Key not found”
Заголовок раздела «”Key not found”»- Ключ мог истечь (TTL)
- Проверьте правильность написания ключа
- Убедитесь что вы обращаетесь к правильному окружению
См. также
Заголовок раздела «См. также»- Runtime SDK — полный API reference
- Environment Variables — статическая конфигурация
- Окружения — управление окружениями проекта
- Edge Runtime — Runtime API