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

Хранение состояния

Рано или поздно ваш бот захочет что-то запоминать между рестартами: кулдауны, XP-очки, настройки гильдий, список подписчиков. Этот гайд — про выбор хранилища под разные задачи.

Что хранимГде
Кулдауны на команды (сотни мс жизни)В памяти (Map)
Состояние игры в чате (живёт минуты)В памяти + периодическое сохранение
Настройки гильдии, XP, статистикаSQLite локально
Распределённый бот на нескольких инстансахRedis / Postgres

Не усложняйте сразу — начните с SQLite и разовьёте если упрётесь.

Годится для всего что живёт короче минут-часов и можно потерять при рестарте.

const cooldowns = new Map(); // key → timestamp
const gameState = new Map(); // channelId → текущая партия
function setCooldown(key, ms) {
cooldowns.set(key, Date.now() + ms);
}

Не забудьте чистить — без eviction’а Map растёт бесконечно:

setInterval(() => {
const now = Date.now();
for (const [k, v] of cooldowns) {
if (v < now) cooldowns.delete(k);
}
}, 60_000);

Идеальный старт. Файл рядом с ботом, нулевая настройка, тысячи записей в секунду.

CREATE TABLE IF NOT EXISTS xp (
guild_id TEXT NOT NULL,
user_id TEXT NOT NULL,
username TEXT,
xp INTEGER NOT NULL DEFAULT 0,
level INTEGER NOT NULL DEFAULT 0,
last_gain_at INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (guild_id, user_id)
);
CREATE INDEX IF NOT EXISTS idx_xp_guild_sorted
ON xp(guild_id, xp DESC);

SQLite-файл можно просто копировать, но не во время транзакции. Безопасный способ — команда VACUUM INTO:

Окно терминала
sqlite3 data/bot.sqlite "VACUUM INTO 'data/backup-$(date +%F).sqlite';"

Поставьте на cron раз в день.

Нужен, когда:

  • Бот шардирован на несколько процессов (им надо делить состояние)
  • Нужны TTL’ы «из коробки» (кулдауны, блэк-листы)
  • Частые инкременты / счётчики под нагрузкой
import {Redis} from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
// Кулдаун с TTL
async function tryCommand(userId, cmd, windowSec = 3) {
const key = `cd:${userId}:${cmd}`;
const ok = await redis.set(key, '1', 'EX', windowSec, 'NX');
return ok === 'OK'; // false — ещё в кулдауне
}
// Атомарный инкремент счётчика
const newCount = await redis.incr(`msgcount:${channelId}`);

Подходит когда:

  • Данных десятки-сотни тысяч записей
  • Нужны сложные выборки (top-N по фильтру, джойны)
  • Несколько инстансов бота пишут параллельно
  • Нужна репликация, бэкап через pg_dump

Для старта бота — overkill. Переходите на Postgres, когда SQLite начнёт упираться или появятся несколько инстансов.

Gateway присылает GUILD_CREATE со списком участников, каналов, ролей — не надо это отдельно дублировать в БД, это есть в памяти. БД нужна для того, что Gateway не расскажет: пользовательские настройки, XP, кастомные команды.

Хранить в БДНе хранить (брать из памяти / Gateway)
XP / очки / бейджиИмя участника (msg.author.username)
Настройки префикса бота по гильдииСписок каналов (из GUILD_CREATE)
Напоминания, таймерыОнлайн / presence
Reaction-role конфигурацияСписок ролей гильдии

Когда бот вырастет до 2500+ гильдий, платформа попросит шардирование — несколько процессов делят гильдии по guild_id % N. Для этого состояние должно быть внешним (Redis/Postgres), иначе шарды будут друг друга не видеть. Если пишете бот с прицелом на большое распространение — сразу на Redis/Postgres, не изобретайте миграцию с SQLite через год.

Маленький бот на один-два сервера? SQLite, не думайте.