随着业务增长,某个动作常常需要触发一串后续流程:下单后记账、发券、发消息;用户注册后送积分、初始化偏好。把这些都写在视图里会迅速变成“意大利面”。Django 的 signals 能把“事件”和“响应”解耦,保留可维护性与扩展性。
1)哪些事适合用信号?
2)最常用的四个内置信号
pre_save
/ post_save
:处理“保存前规范化、保存后副作用”。pre_delete
/ post_delete
:做级联清理、资源回收。from django.db.models.signals import post_save
from django.dispatch import receiver, Signal
from .models import Order
order_paid = Signal() # 自定义领域事件
@receiver(post_save, sender=Order)
def emit_order_paid(sender, instance, created, **kwargs):
if not created and instance.status == "PAID":
order_paid.send(sender=Order, order_id=instance.id)
@receiver(order_paid)
def grant_coupon(sender, order_id, **kwargs):
# 查询订单并发放优惠券、记录审计
pass
3)事务与幂等:避免“幽灵副作用”
与数据库写入强相关的信号监听应放在事务提交后执行,否则回滚会留下副作用。可用事务钩子在提交时触发,或在信号处理函数中做“是否已处理”的幂等检查(例如基于唯一键的处理日志)。
4)同步还是异步?
副作用耗时较长(发邮件、推送、图像处理)时,用任务队列承接,在信号里只做事件入队,减少请求延迟。对强实时的本地缓存失效、轻量埋点则直接同步完成。
5)组织方式与可测试性
将信号接收器集中在独立模块中,并在应用 ready()
中显式导入,避免“导入顺序之谜”。为每个接收器写单元测试:构造事件 → 断言副作用是否发生或入队。
6)观测与降噪
为信号处理设置统一日志前缀,记录事件类型、关键 ID、延迟与异常;异常不要吞掉,捕获后上报并可重试。对高频低价值事件做采样,避免日志爆炸。
用 signals 的目标不是“到处发通知”,而是把副作用从主流程里轻巧地“拿出来”:主流程保持清晰稳定,副作用各就各位、可替换、可扩展。当业务新增“再发一条站内信”的需求时,只需增加一个接收器,而不是拆开核心流程重写一遍。