用 Django Signals 做到“低耦合高反应”:从事件到一致性
随着业务增长,某个动作常常需要触发一串后续流程:下单后记账、发券、发消息;用户注册后送积分、初始化偏好。把这些都写在视图里会迅速变成“意大利面”。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 的目标不是“到处发通知”,而是把副作用从主流程里轻巧地“拿出来”:主流程保持清晰稳定,副作用各就各位、可替换、可扩展。当业务新增“再发一条站内信”的需求时,只需增加一个接收器,而不是拆开核心流程重写一遍。
评论 0