Django Signals vs Celery Tasks w 2026: Kiedy Stosować i Pytania Rekrutacyjne

Kompleksowe porównanie Django Signals i Celery Tasks w 2026 roku. Kiedy używać sygnałów, a kiedy zadań asynchronicznych, z przykładami kodu i pytaniami na rozmowę kwalifikacyjną.

Django Signals vs Celery Tasks 2026

Django Signals i Celery Tasks obsługują zdarzenia w aplikacjach Django, ale rozwiązują różne problemy. Sygnały wykonują się synchronicznie w ramach cyklu request-response, podczas gdy Celery przekazuje pracę do workerów działających w tle. Wybór niewłaściwego narzędzia prowadzi do wolnych odpowiedzi, wyścigów danych lub niepotrzebnie skomplikowanej architektury.

Szybka Zasada Decyzyjna

Django Signals należy stosować do lekkich, synchronicznych efektów ubocznych, które muszą zakończyć się przed odpowiedzią. Celery jest odpowiedni dla wszystkiego, co trwa dłużej niż 100ms, wymaga zewnętrznych serwisów lub może zawieść niezależnie od głównego żądania.

Jak Działają Django Signals Pod Maską

Django Signals implementują wzorzec obserwatora. Gdy model zapisuje się, usuwa lub gdy rozpoczyna się lub kończy żądanie, Django wysyła sygnał. Każda funkcja podłączona do tego sygnału wykonuje się natychmiast, w tej samej transakcji bazodanowej i tym samym wątku.

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):
    # Wykonuje się synchronicznie po Product.save() commit
    cache_key = f"product:{instance.id}"
    cache.delete(cache_key)
    # Również unieważnia listę kategorii
    cache.delete(f"category:{instance.category_id}:products")

Odbiornik sygnału działa wewnątrz tej samej transakcji bazodanowej. Jeśli transakcja zostanie wycofana, efekty handlera sygnału pozostają. Ma to znaczenie dla unieważniania cache: cache zostaje wyczyszczony, ale zmiana w bazie danych nigdy nie zostaje utrwalona. Rezultatem jest brak w cache, który ładuje nieaktualne dane.

Django 5.2 wprowadziło transaction.on_commit() aby temu zaradzić. Opakowanie logiki sygnału w on_commit zapewnia wykonanie tylko po udanym commicie:

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):
    # Odrocz do momentu pomyślnego commitu transakcji
    transaction.on_commit(
        lambda: reindex_product.delay(instance.id)
    )

Ten wzorzec łączy sygnały i Celery: sygnał uruchamia się synchronicznie, ale właściwa praca wykonuje się asynchronicznie po commicie transakcji.

Model Wykonywania Zadań Celery

Celery uruchamia zadania w oddzielnych procesach workerów. Widok Django kolejkuje zadanie poprzez serializację argumentów do brokera wiadomości (Redis lub RabbitMQ). Worker pobiera wiadomość i wykonuje zadanie niezależnie od oryginalnego żądania 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):
    """Wysyła email potwierdzający po złożeniu zamówienia."""
    try:
        order = Order.objects.select_related('user').get(id=order_id)
        send_mail(
            subject=f"Zamówienie #{order.id} Potwierdzone",
            message=f"Twoje zamówienie na kwotę {order.total} zostało złożone.",
            from_email="orders@example.com",
            recipient_list=[order.user.email],
        )
    except Order.DoesNotExist:
        # Zamówienie usunięte przed wykonaniem zadania
        return
    except Exception as exc:
        # Ponów przy przejściowych błędach (timeout SMTP, itp.)
        raise self.retry(exc=exc)

Zadanie działa w innym procesie, potencjalnie na innej maszynie. Nie ma dostępu do oryginalnego kontekstu żądania. Jeśli zamówienie zostanie usunięte między kolejkowaniem a wykonaniem, zadanie musi obsłużyć to odpowiednio.

Celery 5.4 (aktualna stabilna wersja na wrzesień 2026) dodało ulepszone typowanie zadań i lepszą integrację z Django poprzez django-celery-results do przechowywania wyników zadań w bazie danych.

Porównanie Charakterystyk Wydajnościowych

Sygnały dodają opóźnienie do żądania. Każdy podłączony odbiornik wykonuje się przed zwróceniem odpowiedzi. Przy trzech odbiornikach średnio po 50ms każdy, żądanie trwa 150ms dłużej.

Celery dodaje minimalne opóźnienie (typowo 1-5ms na kolejkowanie), ale wprowadza ewentualną spójność. Email wysyła się "w końcu", nie przed odpowiedzią.

CzynnikDjango SignalsCelery Tasks
WykonanieSynchroniczne, ten sam procesAsynchroniczne, proces workera
Wpływ na opóźnienieDodaje do czasu żądania~1-5ms narzut kolejkowania
Obsługa błędówPrzerywa żądaniePonawia niezależnie
Zakres transakcjiWewnątrz transakcjiPoza transakcją
Wywołania zewnętrznych serwisówBlokuje odpowiedźDziała w tle
ZłożonośćMinimalna konfiguracjaWymaga brokera + workerów

Kiedy Sygnały Są Właściwym Wyborem

Sygnały pasują do scenariuszy, gdzie efekt uboczny musi zakończyć się przed zwróceniem odpowiedzi i jest lekki obliczeniowo.

Unieważnianie Cache

Kiedy model się zmienia, powiązany cache musi zostać unieważniony. Jeśli używa się bazy danych jako backendu cache lub prostych operacji kasowania, sygnały działają dobrze:

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")

Aktualizacja Pól Pochodnych

Obliczanie i przechowywanie wartości pochodnych utrzymuje je zsynchronizowane bez dodatkowych zapytań do bazy:

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)

Logowanie Audytu

Dla prostego logowania audytu, gdzie zapis logu musi być częścią tej samej transakcji:

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()
    )

Kiedy Celery Jest Właściwym Wyborem

Celery jest odpowiednie dla wszystkiego, co nie powinno blokować odpowiedzi użytkownika lub może zawieść niezależnie.

Wysyłanie Emaili

Serwery SMTP mogą być wolne lub niedostępne. Przekazanie tego do Celery oznacza, że użytkownik otrzymuje natychmiastową odpowiedź:

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="Witamy w Serwisie",
            body="Witamy...",
            to=[user.email]
        )
        msg.attach_alternative(html_content, "text/html")
        msg.send()
    except User.DoesNotExist:
        return  # Użytkownik usunięty, pomiń
    except Exception as exc:
        raise self.retry(exc=exc)

Przetwarzanie Obrazów

Zmiana rozmiaru, optymalizacja lub konwersja obrazów jest intensywna obliczeniowo:

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:
        # Utwórz miniaturę
        img.thumbnail((300, 300))
        thumb_io = io.BytesIO()
        img.save(thumb_io, format='WEBP', quality=85)
        thumb_io.seek(0)
        
        # Zapisz miniaturę
        img_record.thumbnail.save(
            f"{img_record.id}_thumb.webp",
            thumb_io
        )

Integracje z API Zewnętrznych

Kiedy trzeba zsynchronizować dane z zewnętrznymi serwisami:

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  # Błędy 4xx nie powinny być ponawiane

Łączenie Sygnałów z Celery

Najpotężniejszy wzorzec używa sygnałów jako wyzwalaczy do kolejkowania zadań Celery. Sygnał zapewnia, że zadanie jest kolejkowane spójnie, gdy model się zmienia:

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:
        # Kolejkuj zadania tylko po pomyślnym commicie
        transaction.on_commit(lambda: (
            send_welcome_email.delay(instance.id),
            sync_to_crm.delay(instance.id)
        ))

Ten wzorzec zapewnia:

  • Zadania są kolejkowane tylko wtedy, gdy użytkownik rzeczywiście zostanie zapisany
  • Jeśli transakcja zostanie wycofana, żadne zadania nie są kolejkowane
  • Główne żądanie kończy się natychmiast
  • Praca w tle obsługuje długotrwałe operacje

Obsługa Błędów i Niezawodność

Awarie Sygnałów

Kiedy handler sygnału zgłasza wyjątek, propaguje się on do wywołującego. Jeśli sygnał jest podłączony do post_save, zapis modelu technicznie się powiódł, ale żądanie się nie powodzi:

python
# Zły wzorzec: sygnał może przerwać operację zapisu
@receiver(post_save, sender=Order)
def notify_warehouse(sender, instance, created, **kwargs):
    if created:
        # Jeśli to się nie powiedzie, cały widok się nie powiedzie
        requests.post("https://warehouse.example.com/orders", json={...})

Zamiast tego używaj obsługi wyjątków lub przekazuj do Celery:

python
# Lepszy wzorzec: obsłuż błędy lub przekaż
@receiver(post_save, sender=Order)
def notify_warehouse(sender, instance, created, **kwargs):
    if created:
        transaction.on_commit(
            lambda: notify_warehouse_task.delay(instance.id)
        )

Ponowienia i Backoff Celery

Celery zapewnia wbudowane mechanizmy ponawiania z wykładniczym backoffem:

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,  # Wykładniczy backoff
    retry_backoff_max=600,  # Maksymalnie 10 minut
    retry_jitter=True  # Dodaje losowość aby uniknąć efektu stada
)
def resilient_task(self, data):
    try:
        process_data(data)
    except PermanentError:
        # Nie ponawiaj błędów permanentnych
        return
    except TransientError as exc:
        raise self.retry(exc=exc)

Testowanie Sygnałów vs Zadań Celery

Testowanie Sygnałów

Sygnały mogą być izolowane podczas testów używając menedżera kontekstu factory_boy lub ręcznego rozłączania:

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")

Testowanie Zadań Celery

Celery dostarcza dekoratora @override_settings i tryb eager do testów:

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()

Gotowy na rozmowy o Django?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Pytania Rekrutacyjne: Django Signals

P: Jaka jest różnica między pre_save a post_save?

O: pre_save uruchamia się przed zapisem danych do bazy, umożliwiając modyfikację danych przed utrwaleniem. post_save uruchamia się po zapisie, gdy instancja ma już klucz główny. Używaj pre_save do walidacji lub transformacji danych, post_save do efektów ubocznych jak unieważnianie cache.

P: Jak zapobiegasz nieskończonym pętlom w sygnałach?

O: Nieskończone pętle występują, gdy handler sygnału wyzwala ten sam sygnał. Rozwiązania obejmują: użycie update_fields w save() i sprawdzanie go w handlerze, użycie Model.objects.filter().update() zamiast save(), lub ustawienie flagi na instancji aby zapobiec re-wejściu.

P: Wyjaśnij transaction.on_commit() w kontekście sygnałów.

O: transaction.on_commit() odkłada wykonanie do momentu pomyślnego commitu bieżącej transakcji bazodanowej. Jest to kluczowe dla sygnałów, które wyzwalają efekty zewnętrzne (jak wysyłanie emaili lub kolejkowanie zadań), ponieważ zapewnia, że efekt uboczny nastąpi tylko wtedy, gdy dane rzeczywiście zostaną utrwalone.

P: Kiedy użyłbyś sygnałów zamiast nadpisania save()?

O: Sygnały są lepsze gdy: zachowanie powinno być generyczne dla wielu modeli, logika powinna być rozdzielona od modelu dla celów testowania, lub gdy komponenty innych firm muszą reagować na zmiany modelu bez modyfikacji oryginalnego kodu.

Pytania Rekrutacyjne: Celery Tasks

P: Jak zapewniasz, że zadanie Celery jest wykonywane dokładnie raz?

O: Celery zapewnia gwarancję dostarczenia "at-least-once", nie "exactly-once". Dla idempotencji: używaj unikalnych identyfikatorów zadań z task_id, sprawdzaj czy praca została już wykonana przed jej wykonaniem, lub używaj acks_late=True z idempotentnymi operacjami.

P: Wyjaśnij różnicę między delay() a apply_async().

O: delay() jest skrótem dla apply_async() z domyślnymi opcjami. apply_async() pozwala na szczegółową kontrolę: countdown dla opóźnionego wykonania, eta dla konkretnego czasu, queue dla przekierowania, expires dla TTL zadania.

P: Jak monitorowałbyś zadania Celery w produkcji?

O: Flower zapewnia monitorowanie w czasie rzeczywistym i interfejs webowy. django-celery-results przechowuje wyniki w bazie danych. Własne hookowanie sygnałów zadań (task_success, task_failure) może wysyłać metryki do Prometheus/Datadog. Monitorowanie kolejki przez brokera (długość kolejki Redis) wskazuje backlog.

P: Co się dzieje, gdy worker Celery umiera w trakcie zadania?

O: Domyślnie zadanie jest tracone. Z acks_late=True, zadanie nie jest potwierdzane dopóki nie zakończy się, więc inny worker może je odebrać. Jednak wymaga to idempotentnych zadań, ponieważ mogą być wykonywane częściowo przed restartowaniem.

P: Jak obsłużysz łańcuch zadań, gdzie jedno zadanie zależy od wyniku innego?

O: Celery dostarcza prymitywy canvas: chain() dla sekwencyjnego wykonania, group() dla równoległego wykonania, chord() dla równoległego z callbackiem. Na przykład: chain(task1.s(arg), task2.s(), task3.s())() przekazuje każdy wynik do następnego zadania.

Drzewo Decyzyjne: Sygnały vs Celery

Przy decydowaniu między Signals a Celery, należy wziąć pod uwagę następujące kryteria:

  1. Czy operacja jest lekka (< 100ms)? Jeśli tak, sygnały są odpowiednie.
  2. Czy wymaga wywołań zewnętrznych API/serwisów? Jeśli tak, użyj Celery.
  3. Czy może zawieść niezależnie od głównego żądania? Jeśli tak, użyj Celery.
  4. Czy musi zakończyć się przed odpowiedzią? Jeśli tak, użyj sygnałów.
  5. Czy wymaga złożonych ponowień/logiki backoff? Jeśli tak, użyj Celery.

Najlepszą praktyką jest często łączenie obu: sygnały jako niezawodne wyzwalacze, które kolejkują zadania Celery dla ciężkiej pracy.

Podsumowanie

Django Signals i Celery Tasks służą różnym celom w architekturze aplikacji. Sygnały są idealne dla lekkich, synchronicznych operacji, które muszą być częścią transakcji bazodanowej. Celery jest niezastąpiony dla długotrwałych operacji, integracji z zewnętrznymi serwisami i wszystkiego, co wymaga niezawodnych ponowień. Najsilniejsze aplikacje Django używają obu: sygnałów jako punktów zaczepienia do wyzwalania zadań Celery, łącząc prostotę sygnałów z mocą przetwarzania w tle Celery.

Wyzwanie dnia

Znajdziesz błąd w Django?

Prawdziwy fragment kodu, ukryty błąd, jedna próba dziennie. Bez konta, żeby spróbować.

Anthony Fillion-Maillet

Autor:

Anthony Fillion-Maillet

Założyciel SharpSkill

Programista fullstack od ponad 10 lat. Prowadzi SharpSkill i odpowiada za wszystko, co się tu ukazuje.

Zaktualizowano 2 września 2026

Tagi

#django
#celery
#python
#backend
#signals

Udostępnij

Powiązane artykuły