post_save信号默认同步执行,需用celery等异步工具配合transaction.on_commit确保事务提交后执行;禁止在信号中直接耗时操作或启线程/协程。

post_save 信号默认是同步执行的,不是异步的
Django 的 post_save 信号本身运行在当前请求线程中,和视图代码一样阻塞主线程。很多人误以为“用了信号就自动异步了”,结果导致视图响应变慢、数据库事务卡住、甚至超时。它只是解耦了逻辑位置,并不改变执行时机。
真正要异步,必须显式交由任务队列(如 Celery、RQ)或线程/进程池调度。信号函数里直接调用耗时操作(比如发邮件、调第三方 API、生成缩略图)会拖垮整个请求。
在 post_save 中安全触发 Celery 任务的写法
核心原则:信号函数必须轻量、无副作用、能快速返回;所有重活交给 delay() 或 apply_async()。
- 确保 Celery worker 已启动,且
BROKER_URL配置正确(Django settings 里) - 信号函数里不要传 model 实例本身(序列化可能失败),改传主键
instance.pk和模型名(如"myapp.MyModel") - 任务函数内部再通过
MyModel.objects.get(pk=pk)重新获取实例——避免信号触发时对象状态不一致(比如事务未提交) - 加 try/except 包裹
delay()调用,防止 broker 不可用时崩掉信号流程
示例:
@receiver(post_save, sender=Article)
def trigger_article_processing(sender, instance, created, **kwargs):
if created:
try:
process_article_task.delay(model_name="myapp.Article", obj_id=instance.pk)
except Exception as e:
# 记录日志,但不抛出,避免中断信号链
logger.error(f"Failed to queue article task: {e}")
为什么不能在信号里直接用 threading 或 asyncio.create_task
在 Django 请求上下文中直接启线程或协程风险很高:
-
threading.Thread启动后,主线程结束,子线程可能被强制终止(尤其 Gunicorn/uWSGI 多进程下) -
asyncio.create_task()在同步视图中没 event loop,或 loop 已关闭,会报RuntimeError: no running event loop - 数据库连接在线程间不安全,容易出现
InterfaceError: connection already closed - 无法可靠捕获异常、重试、监控任务状态
这些方式只适合开发调试或极简单场景(比如本地脚本),生产环境请坚持用 Celery / RQ。
常见坑:信号重复触发与事务边界问题
post_save 在 bulk_create、update()、管理后台保存时行为不一致,容易漏触发或误触发:
-
bulk_create默认不触发post_save(除非加send_post_save_signal=True,且仅 Django ≥ 4.2) -
model.save(update_fields=[...])仍会触发信号,但你可能以为“部分更新就不该走” - 如果信号函数里又调
instance.save(),会再次触发自身,造成无限递归(需用dispatch_uid或标志位规避) - 事务未提交前,Celery 任务可能查不到刚创建的记录(解决办法:用
transaction.on_commit()包裹delay())
推荐写法:
@receiver(post_save, sender=Order)
def send_order_confirmation(sender, instance, created, **kwargs):
if created:
transaction.on_commit(
lambda: send_order_email_task.delay(order_id=instance.pk)
)
这个 on_commit 是关键,它确保只有事务成功提交后才发任务,避免查不到数据或重复发送。
信号机制本身不难,难的是理清它和事务、并发、任务生命周期的交界点。别省那几行 on_commit 或主键传参,线上出问题时回溯起来非常痛苦。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











