Django Signals vs Celery Tasks 2026: Khi Nào Sử Dụng Cái Nào

Tìm hiểu khi nào nên sử dụng Django signals so với Celery tasks. Bao gồm xử lý event đồng bộ vs bất đồng bộ, ảnh hưởng hiệu suất, và câu hỏi phỏng vấn cho lập trình viên Django.

Sơ đồ kiến trúc Django signals vs Celery tasks cho thấy các pattern xử lý event đồng bộ và bất đồng bộ

Django signals và Celery tasks đều xử lý các event trong ứng dụng Django, nhưng chúng giải quyết các vấn đề khác nhau. Signals được thực thi đồng bộ trong chu kỳ request-response, trong khi Celery chuyển công việc sang background workers. Việc chọn sai công cụ dẫn đến response chậm, race conditions, hoặc kiến trúc phức tạp không cần thiết.

Quy Tắc Quyết Định Nhanh

Sử dụng Django signals cho các side effects nhẹ, đồng bộ mà phải hoàn thành trước response. Sử dụng Celery cho bất cứ thứ gì mất hơn 100ms, liên quan đến dịch vụ bên ngoài, hoặc có thể thất bại độc lập với request chính.

Cách Django Signals Hoạt Động Bên Trong

Django signals triển khai observer pattern. Khi một model được lưu, xóa, hoặc khi request bắt đầu hoặc kết thúc, Django gửi một signal. Bất kỳ hàm nào được kết nối với signal đó sẽ thực thi ngay lập tức, trong cùng một database transaction và cùng một thread.

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):
    # Chạy đồng bộ sau khi Product.save() commit
    cache_key = f"product:{instance.id}"
    cache.delete(cache_key)
    # Cũng invalidate listing danh mục
    cache.delete(f"category:{instance.category_id}:products")

Signal receiver chạy bên trong cùng một database transaction. Nếu transaction rollback, các hiệu ứng của signal handler vẫn còn. Điều này quan trọng cho cache invalidation: cache bị xóa, nhưng thay đổi database không bao giờ được lưu. Kết quả là cache miss tải lại dữ liệu cũ.

Django 5.2 giới thiệu transaction.on_commit() để giải quyết vấn đề này. Bọc logic signal trong on_commit đảm bảo thực thi chỉ sau khi commit thành công:

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):
    # Trì hoãn cho đến khi transaction commit thành công
    transaction.on_commit(
        lambda: reindex_product.delay(instance.id)
    )

Pattern này kết nối signals và Celery: signal được kích hoạt đồng bộ, nhưng công việc thực tế xảy ra bất đồng bộ sau khi transaction commit.

Mô Hình Thực Thi Celery Task

Celery chạy các tasks trong các tiến trình worker riêng biệt. Một Django view đưa task vào hàng đợi bằng cách serialize các đối số của nó thành message broker (Redis hoặc RabbitMQ). Worker nhận message và thực thi task độc lập với HTTP request ban đầu.

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):
    """Gửi email xác nhận sau khi đặt hàng."""
    try:
        order = Order.objects.select_related('user').get(id=order_id)
        send_mail(
            subject=f"Đơn hàng #{order.id} Đã Xác Nhận",
            message=f"Đơn hàng của bạn với tổng {order.total} đã được đặt.",
            from_email="orders@example.com",
            recipient_list=[order.user.email],
        )
    except Order.DoesNotExist:
        # Đơn hàng đã bị xóa trước khi task chạy, bỏ qua
        return
    except Exception as exc:
        # Retry khi có lỗi tạm thời (SMTP timeout, v.v.)
        raise self.retry(exc=exc)

Task chạy trong một tiến trình khác, có thể trên một máy khác. Nó không có quyền truy cập vào ngữ cảnh request ban đầu. Nếu đơn hàng bị xóa giữa thời điểm enqueue và thực thi, task phải xử lý điều đó một cách hợp lý.

Celery 5.4 (phiên bản ổn định hiện tại tính đến tháng 9 năm 2026) đã thêm cải tiến task typing và tích hợp Django tốt hơn thông qua django-celery-results để lưu trữ kết quả task trong database.

So Sánh Đặc Điểm Hiệu Suất

Signals thêm độ trễ vào request. Mỗi receiver được kết nối thực thi trước khi response được trả về. Với ba receivers trung bình 50ms mỗi cái, request mất thêm 150ms.

Celery thêm độ trễ tối thiểu (thường 1-5ms để enqueue), nhưng giới thiệu eventual consistency. Email được gửi "cuối cùng," không phải trước response.

Yếu TốDjango SignalsCelery Tasks
Thực thiĐồng bộ, cùng tiến trìnhBất đồng bộ, tiến trình worker
Ảnh hưởng độ trễThêm vào thời gian request~1-5ms overhead enqueue
Xử lý lỗiPhá vỡ requestRetry độc lập
Phạm vi transactionBên trong transactionBên ngoài transaction
Gọi dịch vụ bên ngoàiChặn responseChạy trong background
Độ phức tạpSetup tối thiểuYêu cầu broker + workers

Khi Nào Signals Là Lựa Chọn Đúng

Signals phù hợp với các tình huống mà side effect phải hoàn thành trước khi tiếp tục và khi lỗi nên hủy bỏ hoạt động.

Cache invalidation hoạt động tốt với signals. Khi Product được cập nhật, biểu diễn được cache phải được invalidate ngay lập tức. Cache cũ thậm chí trong vài giây gây ra hiển thị giá hoặc số lượng tồn kho sai.

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 Product

@receiver([post_save, post_delete], sender=Product)
def clear_product_cache(sender, instance, **kwargs):
    cache.delete(f"product:{instance.id}")
    cache.delete(f"product:{instance.slug}")
    # Xóa bất kỳ cache danh sách nào có thể bao gồm sản phẩm này
    cache.delete_pattern(f"products:category:{instance.category_id}:*")

Audit logging nơi log phải tồn tại trước khi response được trả về cũng phù hợp với signals. Nếu admin xóa user, audit log nên ghi lại việc xóa đó một cách nguyên tử với hoạt động xóa.

Denormalization của các trường trên các model liên quan được hưởng lợi từ signals. Khi trạng thái Order thay đổi thành "shipped," cập nhật trường denormalized last_shipped_at trên Customer xảy ra ngay lập tức.

Sẵn sàng chinh phục phỏng vấn Django?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

Khi Nào Celery Tasks Là Lựa Chọn Đúng

Celery phù hợp với các tình huống liên quan đến dịch vụ bên ngoài, tính toán chạy lâu, hoặc các hoạt động có thể thất bại và retry độc lập.

Gửi email không nên chặn requests. Máy chủ SMTP có độ trễ không thể đoán trước, đôi khi timeout, và giới hạn rate người gửi. Celery task sẽ retry email thất bại mà không ảnh hưởng đến trải nghiệm người dùng.

Tạo PDF cho hóa đơn hoặc báo cáo mất vài giây. Tạo đồng bộ làm UI cảm thấy bị hỏng. Enqueue task, trả về trạng thái "processing," và cho phép người dùng tải xuống khi sẵn sàng.

Gọi API bên thứ ba thuộc về Celery. Gọi Stripe, Twilio, hoặc bất kỳ dịch vụ bên ngoài nào đồng bộ liên kết thời gian response với thời gian của họ. Khi API của họ chậm lại, ứng dụng cũng chậm lại.

python
# tasks.py
from celery import shared_task
import stripe
from .models import Subscription

@shared_task(bind=True, max_retries=5, retry_backoff=True)
def sync_stripe_subscription(self, subscription_id: int):
    """Đồng bộ subscription local với trạng thái Stripe."""
    try:
        sub = Subscription.objects.get(id=subscription_id)
        stripe_sub = stripe.Subscription.retrieve(sub.stripe_id)
        sub.status = stripe_sub.status
        sub.current_period_end = stripe_sub.current_period_end
        sub.save(update_fields=['status', 'current_period_end'])
    except stripe.error.RateLimitError as exc:
        # Stripe giới hạn rate, retry với exponential backoff
        raise self.retry(exc=exc)
    except Subscription.DoesNotExist:
        return  # Subscription đã bị xóa local, không có gì để đồng bộ

Tham số retry_backoff=True trong Celery 5.x kích hoạt exponential backoff: retry đầu tiên sau 1 giây, sau đó 2, sau đó 4, đến tối đa. Điều này ngăn việc hammering API bị rate-limited.

Pattern Hybrid: Signals Kích Hoạt Tasks

Kiến trúc robust nhất sử dụng signals cho phối hợp nhẹ, ngay lập tức và Celery cho công việc nặng thực tế.

python
# signals.py
from django.db import transaction
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Order
from .tasks import (
    send_order_confirmation,
    notify_warehouse,
    update_analytics,
)

@receiver(post_save, sender=Order)
def on_order_created(sender, instance, created, **kwargs):
    if not created:
        return  # Chỉ xử lý đơn hàng mới
    
    # Enqueue tất cả công việc async sau khi transaction commit
    transaction.on_commit(lambda: (
        send_order_confirmation.delay(instance.id),
        notify_warehouse.delay(instance.id),
        update_analytics.delay('order_created', instance.id),
    ))

Pattern này đảm bảo:

  • Order tồn tại trong database trước khi tasks chạy (transaction đã commit)
  • Không có công việc async nào xảy ra nếu transaction rollback
  • HTTP response được trả về ngay lập tức
  • Mỗi task có thể thất bại và retry độc lập

Câu Hỏi Phỏng Vấn về Django Signals vs Celery

Phỏng vấn kỹ thuật cho vị trí Django thường khám phá sự khác biệt này. Dưới đây là các câu hỏi phân biệt ứng viên có kinh nghiệm với những người chỉ học thuộc tài liệu.

Q: Signal handler gửi email. Transaction database rollback. Điều gì xảy ra?

Email vẫn được gửi. Signal handlers thực thi trong transaction, không phải sau commit. Email được gửi đi, nhưng thay đổi database kích hoạt nó không bao giờ được lưu. Để sửa điều này, bọc lệnh gọi email trong transaction.on_commit(), hoặc tốt hơn, enqueue Celery task bên trong on_commit().

Q: Làm thế nào để ngăn signal firing trong các hoạt động bulk?

bulk_create(), bulk_update(), và QuerySet.update() của Django không kích hoạt signals. Đây là cố ý để tối ưu hiệu suất. Nếu cần hành vi signal, lặp và lưu từng cái (chấp nhận chi phí hiệu suất) hoặc dispatch signal thủ công sau hoạt động bulk.

Q: Celery task tham chiếu request.user. Tại sao nó thất bại?

Celery tasks chạy trong tiến trình riêng biệt không có quyền truy cập vào ngữ cảnh HTTP request. Truyền user ID làm đối số task và lấy đối tượng User bên trong task. Không bao giờ truyền các instance model Django trực tiếp làm đối số task, vì chúng có thể trở nên cũ hoặc không serialize được.

Q: Làm thế nào để test signal handlers một cách cô lập?

Disconnect signal, gọi hàm handler trực tiếp với các đối số mock, sau đó reconnect. Các phương thức Signal.disconnect()Signal.connect() của Django cho phép điều này. Decorator @factory.django.mute_signals từ thư viện factory_boy cũng giúp bằng cách tạm thời tắt tiếng signals trong quá trình test setup.

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

class SignalTests(TestCase):
    def test_cache_invalidation_called(self):
        # Disconnect để ngăn firing tự động
        post_save.disconnect(invalidate_product_cache, sender=Product)
        
        try:
            product = Product.objects.create(name="Test", price=100)
            
            with patch('myapp.signals.cache') as mock_cache:
                # Gọi handler trực tiếp
                invalidate_product_cache(
                    sender=Product,
                    instance=product,
                    created=True
                )
                mock_cache.delete.assert_called()
        finally:
            # Reconnect cho các test khác
            post_save.connect(invalidate_product_cache, sender=Product)

Các Anti-Pattern Phổ Biến Cần Tránh

Circular signal chains xảy ra khi Signal A kích hoạt save trên Model B, mà signal của nó kích hoạt save trên Model A. Đệ quy tiếp tục cho đến khi stack overflow hoặc recursion guard dừng nó. Thiết kế signals để terminal: chúng quan sát và phản ứng, nhưng không kích hoạt thêm các thay đổi có thể quan sát được.

Tính toán nặng trong signals chặn mọi request kích hoạt signal. Signal resize hình ảnh, tạo thumbnails, hoặc gọi API bên ngoài nên enqueue Celery task thay thế.

Lỗi signal im lặng ẩn bugs. Theo mặc định, Django bắt exceptions trong signal handlers và ghi log chúng, cho phép request tiếp tục. Điều này che giấu lỗi. Cân nhắc xem handler thất bại có nên hủy bỏ hoạt động hay thực sự là best-effort.

python
# settings.py
# Làm cho exception signal handler propagate (fail fast trong development)
DEBUG = True  # Trong dev, exceptions sẽ propagate tự nhiên

# Trong production, sử dụng Sentry hoặc tương tự để bắt signal errors
import sentry_sdk
sentry_sdk.init(dsn="...")

Bắt đầu luyện tập!

Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.

Framework Quyết Định cho Xử Lý Event Django

Việc chọn giữa signals và Celery trở nên đơn giản với cách tiếp cận có hệ thống:

  • Sử dụng signals khi: hoạt động phải hoàn thành trước response, mất dưới 50ms, chỉ liên quan đến hoạt động database hoặc cache local, và lỗi nên hủy bỏ hoạt động chính
  • Sử dụng Celery khi: hoạt động có thể xảy ra sau đó, liên quan đến dịch vụ bên ngoài, mất hơn 100ms, được hưởng lợi từ logic retry, hoặc không nên chặn người dùng
  • Sử dụng signals kích hoạt Celery khi: cần xác nhận ngay lập tức nhưng công việc thực tế là async, hoặc khi nhiều async tasks nên được kích hoạt từ một database event
  • Tránh signals hoàn toàn khi: logic đủ phức tạp để đảm bảo các lệnh gọi service rõ ràng, khi debugging signal chains trở nên khó khăn, hoặc khi hiệu ứng tương tự có thể đạt được với một lệnh gọi phương thức đơn giản

Tính năng background tasks của Django 6.0 cung cấp một giải pháp trung gian: hỗ trợ async task tích hợp sẵn mà không có overhead vận hành của Celery. Đối với các dự án greenfield bắt đầu vào cuối năm 2026, hãy đánh giá xem background tasks native của Django có đáp ứng yêu cầu không trước khi thêm Celery.

Thử thách hôm nay

Bạn có tìm ra lỗi trong Django không?

Một đoạn mã thật, một lỗi ẩn, mỗi ngày một lượt. Không cần tài khoản để thử.

Anthony Fillion-Maillet

Viết bởi

Anthony Fillion-Maillet

Người sáng lập SharpSkill

Lập trình viên fullstack hơn 10 năm. Anh điều hành SharpSkill và chịu trách nhiệm về mọi nội dung đăng tại đây.

Cập nhật ngày 2 tháng 9, 2026

Thẻ

#django
#celery
#signals
#async
#python
#best-practices

Chia sẻ

Bài viết liên quan