bigywater.tech
Django · 107

Signal 是否适合处理复杂业务

信号能解耦,也能把业务流程藏起来。这篇不下定论,而是把「复杂业务放进信号」会遇到的几种情形一一跑出来:隐藏的查询、失败的传播、递归、顺序,最后对比同样逻辑写成显式函数的样子。

1 → 7
挂上 3 个 receiver 后,同一行 create() 的查询数
RecursionError
receiver 里 save() 同一对象 → 无限递归
connect 顺序
receiver 的执行顺序,业务依赖它就是隐性耦合

调用处看不到的事

实测 · 本机执行输出
没有 receiver:一次 create() 的查询数 1
挂上 3 个 receiver 后同一行 create() 的查询数 7  (调用处代码完全没变)

失败怎么传播

实测 · 本机执行输出
create() 抛出: 库存服务不可用
没有 atomic:订单已经写入了吗? True
外面包了 atomic:订单还在吗? False

post_save 用的是 send(),receiver 的异常会直接抛给 save() 的调用方。有没有外层 atomic() 决定了是「半截业务」还是「整体回滚」。

在信号里保存自己

实测 · 本机执行输出
RecursionError: maximum recursion depth exceeded (递归上限 1000 )
改用 queryset.update()(不发信号):库里 status = paid

要在保存后改自己的字段,用 queryset.update()(不发信号),或者用标志位跳过。

执行顺序

实测 · 本机执行输出
connect 顺序 ['a', 'b'] → 执行顺序 ['a', 'b']
connect 顺序 ['b', 'a'] → 执行顺序 ['b', 'a']

显式的服务函数

实测 · 本机执行输出
place_order() 查询数: 6 (同样的写入,全部写在一个函数里,调用处能看到)

信号更适合「发布事件、别的模块可以订阅」这类松耦合的场景(比如缓存失效、日志、索引更新),而不是有先后依赖、必须全部成功的核心业务。这是设计判断,不是本文实验的结论;实验只说明了它的代价:隐藏的查询、失败传播、递归风险和顺序耦合。

分步演示

复杂业务放进信号
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7 / Django 6.1.1(默认用 SQLite,其它数据库会在文中注明),用 django.test.Client 或真实的本机服务器进程实测;计时用 time.perf_counter,数字每次运行会有小幅抖动。SQLite;业务是虚构的订单 / 库存 / 日志三张表
    • 没有验证:真实项目里 receiver 的 connect 顺序由什么决定、信号在 Celery 任务里的表现、on_commit 版本(第 106 篇)的失败传播、性能上的差别
    京ICP备2026067502号-1京公网安备11010502063487号