Очереди задач на списках
Самая простая очередь задач в 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-список и восстановление зависших задач.