У разработчиков есть привычка: если задача требует очереди — сразу тянуть RabbitMQ или Redis. И забывают, что для маленького сайта с парой тысяч задач в день есть решение проще.

Когда очередь на самом деле не нужна

Маленький сайт, которому нужно раз в час отправить email-рассылку. Вы ставите RabbitMQ, Redis, докер-контейнер с воркером. И вот уже три сервиса вместо одного. А что, если можно обойтись SQLite?

Не в качестве временного решения. В качестве постоянного. Звучит дико? Давайте разберёмся.

Как это работает

Берём таблицу tasks. Поля: id, payload (JSON с данными задачи), status, created_at, processed_at. Одновременно с этим создаём механизм, который забирает одну задачу, выполняет её и помечает как выполненную.

На первый взгляд всё просто. Но есть нюанс: если два воркера стартуют одновременно, они могут забрать одну и ту же задачу. Решение — блокировка на уровне записи.

SQLite поддерживает SELECT ... FOR UPDATE SKIP LOCKED. Это означает: выбрать строку, заблокировать её так, чтобы другие запросы её пропустили. Идеально для одиночного захвата задачи.

Пример кода

Создаём таблицу:

CREATE TABLE tasks (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  payload TEXT NOT NULL,
  status TEXT DEFAULT 'pending',
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  processed_at DATETIME
);

Забираем задачу:

db.transaction(() => {
  const task = db.prepare(`
    SELECT * FROM tasks
    WHERE status = 'pending'
    ORDER BY created_at ASC
    LIMIT 1
  `).get();

  if (task) {
    db.prepare(`
      UPDATE tasks
      SET status = 'processing'
      WHERE id = ?
    `).run(task.id);

    return task;
  }
})();

Обработали — пометили выполненной:

db.prepare(`
  UPDATE tasks
  SET status = 'done', processed_at = CURRENT_TIMESTAMP
  WHERE id = ?
`).run(task.id);

Весь процесс — внутри транзакции. Два параллельных воркера? Второй просто не найдёт задачу в статусе pending и уйдёт на следующую итерацию.

Что это даёт

Во-первых, ноль внешних зависимостей. Один файл базы данных, который вы и так ведёте. Не нужно поднимать отдельный сервис, следить за его uptime, настраивать reconnect логику.

Во-вторых, ACID. Если воркер упадёт посреди обработки — задача останется в статусе processing. При следующем запуске выбираете задачи не по pending, а по processing, где processed_at старше часа. Ручной recovery без дополнительного инструмента.

В-третьих, простота отладки. Посмотреть очередь — обычный SELECT. Добавить задачу вручную — INSERT. Всё через тот же интерфейс, которым вы уже пользуетесь.

Ограничения

Это неEnterprise-решение. Если вам нужны сотни задач в секунду — Redis и RabbitMQ всё ещё правильный выбор. SQLite работает на файловых блокировках, и на высокой конкурентности это станет узким местом.

Но для подавляющего большинства проектов — ежедневные email-рассылки, генерация отчётов, пересчёт аналитики — возможностей SQLite хватит с запасом.

Главное

Мы часто мыслим двумя крайностями: «возьму самое простое» или «возьму самое мощное». И забываем про середину. SQLite как очередь — это пример середины, которая закроет 90% потребностей, а стоить будет ноль дополнительных сервисов.

Знать возможности инструмента важнее, чем следовать списку «лучших практик».