Django Signals vs Celery Tasks у 2026: Коли Використовувати та Питання на Співбесіді

Комплексне порівняння Django Signals та Celery Tasks у 2026 році. Коли використовувати сигнали, а коли асинхронні задачі, з прикладами коду та питаннями на технічну співбесіду.

Django Signals vs Celery Tasks 2026

Django Signals та Celery Tasks обробляють події в Django-додатках, але вирішують різні проблеми. Сигнали виконуються синхронно в межах циклу запит-відповідь, тоді як Celery передає роботу фоновим воркерам. Вибір неправильного інструменту призводить до повільних відповідей, перегонів даних або непотрібно складної архітектури.

Швидке Правило Вибору

Django Signals слід використовувати для легких, синхронних побічних ефектів, які мають завершитися до відповіді. Celery підходить для всього, що триває довше 100мс, потребує зовнішніх сервісів або може завершитися невдачею незалежно від основного запиту.

Як Працюють Django Signals Під Капотом

Django Signals реалізують патерн спостерігача. Коли модель зберігається, видаляється або коли починається чи завершується запит, Django надсилає сигнал. Кожна функція, підключена до цього сигналу, виконується негайно, в тій самій транзакції бази даних і тому ж потоці.

python
# signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.core.cache import cache
from .models import Product

@receiver(post_save, sender=Product)
def invalidate_product_cache(sender, instance, **kwargs):
    # Виконується синхронно після Product.save() commit
    cache_key = f"product:{instance.id}"
    cache.delete(cache_key)
    # Також інвалідує список категорій
    cache.delete(f"category:{instance.category_id}:products")

Отримувач сигналу працює всередині тієї ж транзакції бази даних. Якщо транзакцію буде відкочено, ефекти обробника сигналу залишаться. Це важливо для інвалідації кешу: кеш очищується, але зміна в базі даних ніколи не зберігається. Результатом є промах кешу, який завантажує застарілі дані.

Django 5.2 запровадив transaction.on_commit() для вирішення цієї проблеми. Обгортання логіки сигналу в on_commit забезпечує виконання лише після успішного коміту:

python
# signals.py
from django.db import transaction
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Product
from .tasks import reindex_product

@receiver(post_save, sender=Product)
def handle_product_saved(sender, instance, **kwargs):
    # Відкласти до успішного коміту транзакції
    transaction.on_commit(
        lambda: reindex_product.delay(instance.id)
    )

Цей патерн поєднує сигнали та Celery: сигнал спрацьовує синхронно, але фактична робота виконується асинхронно після коміту транзакції.

Модель Виконання Задач Celery

Celery запускає задачі в окремих процесах воркерів. Django view ставить задачу в чергу шляхом серіалізації аргументів до брокера повідомлень (Redis або RabbitMQ). Воркер підхоплює повідомлення і виконує задачу незалежно від оригінального HTTP-запиту.

python
# tasks.py
from celery import shared_task
from django.core.mail import send_mail
from .models import Order

@shared_task(bind=True, max_retries=3, default_retry_delay=60)
def send_order_confirmation(self, order_id: int):
    """Надсилає підтвердження замовлення після оформлення."""
    try:
        order = Order.objects.select_related('user').get(id=order_id)
        send_mail(
            subject=f"Замовлення #{order.id} Підтверджено",
            message=f"Ваше замовлення на суму {order.total} оформлено.",
            from_email="orders@example.com",
            recipient_list=[order.user.email],
        )
    except Order.DoesNotExist:
        # Замовлення видалено до виконання задачі
        return
    except Exception as exc:
        # Повторити при тимчасових помилках (SMTP timeout тощо)
        raise self.retry(exc=exc)

Задача працює в іншому процесі, потенційно на іншій машині. Вона не має доступу до оригінального контексту запиту. Якщо замовлення буде видалено між постановкою в чергу та виконанням, задача має обробити це коректно.

Celery 5.4 (актуальна стабільна версія на вересень 2026) додала покращену типізацію задач та кращу інтеграцію з Django через django-celery-results для зберігання результатів задач у базі даних.

Порівняння Характеристик Продуктивності

Сигнали додають затримку до запиту. Кожен підключений отримувач виконується до повернення відповіді. При трьох отримувачах у середньому по 50мс кожен, запит триває на 150мс довше.

Celery додає мінімальну затримку (типово 1-5мс на постановку в чергу), але запроваджує кінцеву узгодженість. Email надсилається "врешті-решт", а не до відповіді.

ФакторDjango SignalsCelery Tasks
ВиконанняСинхронне, той самий процесАсинхронне, процес воркера
Вплив на затримкуДодає до часу запиту~1-5мс накладні витрати черги
Обробка помилокПерериває запитПовторює незалежно
Область транзакціїВсередині транзакціїПоза транзакцією
Виклики зовнішніх сервісівБлокує відповідьПрацює у фоні
СкладністьМінімальне налаштуванняПотребує брокера + воркерів

Коли Сигнали Є Правильним Вибором

Сигнали підходять для сценаріїв, де побічний ефект має завершитися до повернення відповіді та є легким обчислювально.

Інвалідація Кешу

Коли модель змінюється, пов'язаний кеш має бути інвалідований. Якщо використовується база даних як бекенд кешу або прості операції видалення, сигнали працюють добре:

python
# signals.py
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from django.core.cache import cache
from .models import Article

@receiver([post_save, post_delete], sender=Article)
def clear_article_cache(sender, instance, **kwargs):
    cache.delete(f"article:{instance.slug}")
    cache.delete("article:list")
    cache.delete(f"author:{instance.author_id}:articles")

Оновлення Похідних Полів

Обчислення та збереження похідних значень підтримує їх синхронізацію без додаткових запитів до бази:

python
# signals.py
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from django.db.models import Sum
from .models import OrderItem, Order

@receiver([post_save, post_delete], sender=OrderItem)
def update_order_total(sender, instance, **kwargs):
    order = instance.order
    total = OrderItem.objects.filter(
        order=order
    ).aggregate(
        total=Sum('price')
    )['total'] or 0
    Order.objects.filter(id=order.id).update(total=total)

Аудит Логування

Для простого аудит-логування, де запис логу має бути частиною тієї ж транзакції:

python
# signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import SensitiveData, AuditLog

@receiver(post_save, sender=SensitiveData)
def log_sensitive_data_change(sender, instance, created, **kwargs):
    AuditLog.objects.create(
        model_name='SensitiveData',
        object_id=instance.id,
        action='created' if created else 'updated',
        timestamp=timezone.now()
    )

Коли Celery Є Правильним Вибором

Celery підходить для всього, що не повинно блокувати відповідь користувача або може завершитися невдачею незалежно.

Надсилання Email

SMTP-сервери можуть бути повільними або недоступними. Передача цього Celery означає, що користувач отримує миттєву відповідь:

python
# tasks.py
from celery import shared_task
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string

@shared_task(bind=True, max_retries=5, default_retry_delay=120)
def send_welcome_email(self, user_id: int):
    from .models import User
    try:
        user = User.objects.get(id=user_id)
        html_content = render_to_string(
            'emails/welcome.html',
            {'user': user}
        )
        msg = EmailMultiAlternatives(
            subject="Ласкаво Просимо до Сервісу",
            body="Ласкаво просимо...",
            to=[user.email]
        )
        msg.attach_alternative(html_content, "text/html")
        msg.send()
    except User.DoesNotExist:
        return  # Користувача видалено, пропустити
    except Exception as exc:
        raise self.retry(exc=exc)

Обробка Зображень

Зміна розміру, оптимізація або конвертація зображень є обчислювально інтенсивними:

python
# tasks.py
from celery import shared_task
from PIL import Image
import io

@shared_task
def process_uploaded_image(image_id: int):
    from .models import UploadedImage
    img_record = UploadedImage.objects.get(id=image_id)
    
    with Image.open(img_record.original.path) as img:
        # Створити мініатюру
        img.thumbnail((300, 300))
        thumb_io = io.BytesIO()
        img.save(thumb_io, format='WEBP', quality=85)
        thumb_io.seek(0)
        
        # Зберегти мініатюру
        img_record.thumbnail.save(
            f"{img_record.id}_thumb.webp",
            thumb_io
        )

Інтеграції з Зовнішніми API

Коли потрібно синхронізувати дані із зовнішніми сервісами:

python
# tasks.py
from celery import shared_task
import httpx

@shared_task(bind=True, max_retries=3, default_retry_delay=300)
def sync_to_crm(self, customer_id: int):
    from .models import Customer
    try:
        customer = Customer.objects.get(id=customer_id)
        with httpx.Client(timeout=30) as client:
            response = client.post(
                "https://api.crm.example.com/contacts",
                json={
                    "email": customer.email,
                    "name": customer.name,
                    "source": "webapp"
                },
                headers={"Authorization": f"Bearer {settings.CRM_API_KEY}"}
            )
            response.raise_for_status()
            customer.crm_id = response.json()['id']
            customer.save(update_fields=['crm_id'])
    except httpx.HTTPStatusError as exc:
        if exc.response.status_code >= 500:
            raise self.retry(exc=exc)
        raise  # 4xx помилки не повинні повторюватися

Поєднання Сигналів з Celery

Найпотужніший патерн використовує сигнали як тригери для постановки задач Celery в чергу. Сигнал забезпечує послідовну постановку задачі в чергу, коли модель змінюється:

python
# signals.py
from django.db import transaction
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import User
from .tasks import send_welcome_email, sync_to_crm

@receiver(post_save, sender=User)
def handle_new_user(sender, instance, created, **kwargs):
    if created:
        # Ставити задачі в чергу лише після успішного коміту
        transaction.on_commit(lambda: (
            send_welcome_email.delay(instance.id),
            sync_to_crm.delay(instance.id)
        ))

Цей патерн забезпечує:

  • Задачі ставляться в чергу лише тоді, коли користувача справді збережено
  • Якщо транзакцію відкочено, жодні задачі не ставляться в чергу
  • Основний запит завершується негайно
  • Фонова робота обробляє тривалі операції

Обробка Помилок та Надійність

Помилки Сигналів

Коли обробник сигналу викидає виняток, він поширюється до викликаючого коду. Якщо сигнал підключено до post_save, збереження моделі технічно успішне, але запит завершується невдачею:

python
# Поганий патерн: сигнал може зламати операцію збереження
@receiver(post_save, sender=Order)
def notify_warehouse(sender, instance, created, **kwargs):
    if created:
        # Якщо це не вдасться, весь view не вдасться
        requests.post("https://warehouse.example.com/orders", json={...})

Замість цього використовуйте обробку винятків або передавайте до Celery:

python
# Кращий патерн: обробити помилки або передати
@receiver(post_save, sender=Order)
def notify_warehouse(sender, instance, created, **kwargs):
    if created:
        transaction.on_commit(
            lambda: notify_warehouse_task.delay(instance.id)
        )

Повторення та Backoff Celery

Celery надає вбудовані механізми повторення з експоненційним backoff:

python
# tasks.py
from celery import shared_task
from celery.exceptions import MaxRetriesExceededError

@shared_task(
    bind=True,
    max_retries=5,
    autoretry_for=(ConnectionError, TimeoutError),
    retry_backoff=True,  # Експоненційний backoff
    retry_backoff_max=600,  # Максимум 10 хвилин
    retry_jitter=True  # Додає випадковість для уникнення ефекту стада
)
def resilient_task(self, data):
    try:
        process_data(data)
    except PermanentError:
        # Не повторювати постійні помилки
        return
    except TransientError as exc:
        raise self.retry(exc=exc)

Тестування Сигналів vs Задач Celery

Тестування Сигналів

Сигнали можна ізолювати під час тестів за допомогою контекстного менеджера factory_boy або ручного від'єднання:

python
# tests/test_signals.py
from django.test import TestCase
from django.db.models.signals import post_save
from unittest.mock import patch
from .models import Product
from .signals import invalidate_product_cache

class ProductSignalTest(TestCase):
    def test_cache_invalidation_on_save(self):
        with patch('django.core.cache.cache.delete') as mock_delete:
            product = Product.objects.create(name="Test", category_id=1)
            mock_delete.assert_any_call(f"product:{product.id}")
            mock_delete.assert_any_call(f"category:1:products")

Тестування Задач Celery

Celery надає декоратор @override_settings та eager-режим для тестів:

python
# tests/test_tasks.py
from django.test import TestCase, override_settings
from unittest.mock import patch
from .tasks import send_welcome_email
from .models import User

@override_settings(CELERY_TASK_ALWAYS_EAGER=True)
class EmailTaskTest(TestCase):
    @patch('django.core.mail.EmailMultiAlternatives.send')
    def test_welcome_email_sent(self, mock_send):
        user = User.objects.create(email="test@example.com")
        send_welcome_email.delay(user.id)
        mock_send.assert_called_once()

Готовий до співбесід з Django?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

Питання на Співбесіді: Django Signals

П: Яка різниця між pre_save та post_save?

В: pre_save спрацьовує до збереження даних у базу, дозволяючи модифікувати дані перед збереженням. post_save спрацьовує після збереження, коли екземпляр вже має первинний ключ. Використовуйте pre_save для валідації або трансформації даних, post_save для побічних ефектів як інвалідація кешу.

П: Як запобігти нескінченним циклам у сигналах?

В: Нескінченні цикли виникають, коли обробник сигналу тригерить той самий сигнал. Рішення включають: використання update_fields у save() та перевірку його в обробнику, використання Model.objects.filter().update() замість save(), або встановлення прапорця на екземплярі для запобігання повторному входу.

П: Поясніть transaction.on_commit() у контексті сигналів.

В: transaction.on_commit() відкладає виконання до успішного коміту поточної транзакції бази даних. Це критично для сигналів, що тригерять зовнішні ефекти (як надсилання email або постановка задач у чергу), оскільки забезпечує, що побічний ефект відбудеться лише тоді, коли дані справді збережено.

П: Коли використовувати сигнали замість перевизначення save()?

В: Сигнали кращі коли: поведінка має бути загальною для кількох моделей, логіка має бути відокремлена від моделі для тестування, або коли сторонні компоненти мають реагувати на зміни моделі без модифікації оригінального коду.

Питання на Співбесіді: Celery Tasks

П: Як забезпечити виконання задачі Celery рівно один раз?

В: Celery надає гарантію доставки "at-least-once", а не "exactly-once". Для ідемпотентності: використовуйте унікальні ідентифікатори задач з task_id, перевіряйте чи роботу вже виконано перед виконанням, або використовуйте acks_late=True з ідемпотентними операціями.

П: Поясніть різницю між delay() та apply_async().

В: delay() є скороченням для apply_async() з типовими опціями. apply_async() дозволяє детальний контроль: countdown для відкладеного виконання, eta для конкретного часу, queue для маршрутизації, expires для TTL задачі.

П: Як моніторити задачі Celery у продакшені?

В: Flower надає моніторинг у реальному часі та веб-інтерфейс. django-celery-results зберігає результати в базі даних. Кастомне підключення до сигналів задач (task_success, task_failure) може надсилати метрики до Prometheus/Datadog. Моніторинг черги через брокера (довжина черги Redis) показує backlog.

П: Що відбувається, коли воркер Celery падає під час виконання задачі?

В: За замовчуванням задача втрачається. З acks_late=True задача не підтверджується поки не завершиться, тому інший воркер може її підхопити. Однак це вимагає ідемпотентних задач, оскільки вони можуть бути частково виконані перед перезапуском.

П: Як обробити ланцюг задач, де одна задача залежить від результату іншої?

В: Celery надає canvas-примітиви: chain() для послідовного виконання, group() для паралельного виконання, chord() для паралельного з callback. Наприклад: chain(task1.s(arg), task2.s(), task3.s())() передає кожен результат наступній задачі.

Дерево Рішень: Сигнали vs Celery

При виборі між Signals та Celery слід враховувати наступні критерії:

  1. Чи операція легка (< 100мс)? Якщо так, сигнали підходять.
  2. Чи потребує викликів зовнішніх API/сервісів? Якщо так, використовуйте Celery.
  3. Чи може завершитися невдачею незалежно від основного запиту? Якщо так, використовуйте Celery.
  4. Чи має завершитися до відповіді? Якщо так, використовуйте сигнали.
  5. Чи потребує складних повторень/логіки backoff? Якщо так, використовуйте Celery.

Найкращою практикою часто є поєднання обох: сигнали як надійні тригери, що ставлять задачі Celery в чергу для важкої роботи.

Підсумок

Django Signals та Celery Tasks служать різним цілям в архітектурі додатків. Сигнали ідеальні для легких, синхронних операцій, що мають бути частиною транзакції бази даних. Celery незамінний для тривалих операцій, інтеграцій із зовнішніми сервісами та всього, що потребує надійних повторень. Найпотужніші Django-додатки використовують обидва підходи: сигнали як хуки для тригерування задач Celery, поєднуючи простоту сигналів з потужністю фонової обробки Celery.

Щоденний виклик

Чи знайдеш ти помилку в Django?

Справжній фрагмент коду, прихована помилка, одна спроба на день. Щоб спробувати, акаунт не потрібен.

Anthony Fillion-Maillet

Автор:

Anthony Fillion-Maillet

Засновник SharpSkill

Fullstack-розробник понад 10 років. Керує SharpSkill і відповідає за все, що тут публікується.

Оновлено 2 вересня 2026 р.

Теги

#django
#celery
#python
#backend
#signals

Поділитися

Пов'язані статті