Очереди задач на списках

Самая простая очередь задач в Redis строится на списке за пять минут. Понимание её работы — фундамент для распределённой обработки.

Один процесс кладёт задачи в список, другой их забирает. Это и есть очередь задач — основа фоновой обработки в любом масштабируемом приложении.

Очередь задач разделяет «приём работы» и «выполнение работы». Веб-приложение быстро отвечает пользователю, а тяжёлую задачу (отправку письма, генерацию отчёта) кладёт в очередь. Отдельные воркеры разбирают очередь в фоне. На списках Redis это делается элементарно.

Простая очередь: продюсер и воркер

# Продюсер кладёт задачи в хвост
RPUSH queue:emails "send:user42:welcome"
RPUSH queue:emails "send:user43:reset"

# Воркер блокируется и ждёт задачу из головы
BLPOP queue:emails 0
1) "queue:emails"
2) "send:user42:welcome"   # забрал, обрабатывает

RPUSH добавляет в хвост, BLPOP блокирующе забирает из головы. Если воркеров несколько, каждую задачу получит ровно один из них — Redis отдаёт элемент только одному ждущему клиенту. Это и есть распределение нагрузки.

   Очередь задач на списке

   Продюсеры           Очередь (список)        Воркеры
   ---------           ----------------        -------
   web1 --RPUSH-->  [t1][t2][t3][t4]  <--BLPOP-- worker1
   web2 --RPUSH-->                    <--BLPOP-- worker2

   Каждую задачу забирает ОДИН воркер.

Проблема простой очереди: падение воркера

У простой схемы есть слабое место. Воркер забрал задачу через BLPOP — задача исчезла из списка. Если воркер упал в процессе обработки, задача потеряна: её уже нет в очереди, но она не выполнена.

Надёжная очередь: reliable queue

Решение — LMOVE (или старый RPOPLPUSH): атомарно переложить задачу из основной очереди в «список обрабатываемых». Воркер берёт задачу, она сразу попадает в processing-список. После успешной обработки воркер удаляет её оттуда. Если воркер упал — задача осталась в processing-списке, и её можно вернуть в очередь.

# Атомарно: взять из очереди и положить в "обрабатываемые"
LMOVE queue:emails processing:w1 LEFT RIGHT
# ... обработали успешно ...
LREM processing:w1 1 "<задача>"   # убрать из обрабатываемых

Демонстрация: надёжная очередь на Python deque

from collections import deque

queue = deque()
processing = deque()
done = []

# Продюсер кладёт задачи
for t in ["email-1", "email-2", "email-3"]:
    queue.append(t)
print("Очередь:", list(queue))

def take_job():
    # атомарно: из queue -> в processing (как LMOVE)
    if not queue:
        return None
    job = queue.popleft()
    processing.append(job)
    return job

def ack(job):
    # успех: убрать из processing (как LREM)
    processing.remove(job)
    done.append(job)

# Воркер 1 берёт и успешно обрабатывает
j = take_job(); print(f"Взял {j}, processing={list(processing)}"); ack(j)

# Воркер 2 берёт, но "падает" (не делает ack)
j = take_job(); print(f"Взял {j} и упал! processing={list(processing)}")

# Восстановление: возвращаем зависшие задачи из processing в очередь
for stuck in list(processing):
    processing.remove(stuck)
    queue.appendleft(stuck)
    print(f"Вернули зависшую задачу: {stuck}")

print("\nОбработано:", done)
print("Очередь после восстановления:", list(queue))
print("Задача упавшего воркера НЕ потеряна — она вернулась в очередь.")

Благодаря processing-списку упавшая задача не исчезает: её видно и можно вернуть в обработку. Это и есть надёжная очередь.

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

Ключ надёжности — атомарность LMOVE: задача никогда не «висит в воздухе» между списками, она всегда либо в очереди, либо в processing. Поскольку Redis однопоточный, перемещение неделимо. Восстановление обычно делает отдельный процесс-«сторож», периодически проверяющий processing-списки на застрявшие задачи (по времени) и возвращающий их. Для более развитой семантики Redis предлагает Streams с consumer groups (следующий урок).

Частые ошибки

  • Простой BLPOP для важных задач. Падение воркера = потеря задачи. Нужен processing-список или Streams.
  • Нет таймаута на «зависшие» задачи. Без сторожа задача в processing застрянет навсегда.
  • Неидемпотентные задачи. При повторе (после возврата из processing) задача выполнится дважды — обработчик должен это переживать.

Best practices

  • Для надёжности используйте паттерн reliable queue с LMOVE и processing-списком.
  • Делайте обработчики идемпотентными — задача может выполниться повторно.
  • Для сложных требований (consumer groups, подтверждения, повторы) переходите на Redis Streams.

Итог: Очередь задач на списке: RPUSH кладёт, BLPOP забирает, каждую задачу получает один воркер. Простая схема теряет задачи при падении воркера; надёжная очередь решает это через атомарный LMOVE в processing-список и восстановление зависших задач.

Проверьте себя
1. Почему простая очередь на BLPOP может потерять задачу?
ABLPOP иногда возвращает неверные данные
BЕсли воркер забрал задачу через BLPOP и упал при обработке, задача уже удалена из очереди и не выполнена
CBLPOP удаляет всю очередь целиком
DBLPOP не работает с несколькими воркерами
2. Как паттерн надёжной очереди (reliable queue) защищает от потери задач?
AДублирует каждую задачу в десять списков
BАтомарно перемещает задачу через LMOVE из очереди в processing-список; при падении воркера задача остаётся там и может быть возвращена
CХранит все задачи в обычной строке
DЗапрещает воркерам падать