实测缓存过期与并发:get_or_set 20 线程并发时 compute 执行 20 次;cache.add 互斥只算 1 次;计算 200 ms、TTL 1 秒时阻塞式回源 p99 205 ms,stale-while-revalidate p99 0.3 ms;set 重置 TTL;缓存里读-改-写会 lost update。
实测用 Redis 缓存 session:key 格式 :1:django.contrib.sessions.cache<key>、TTL 14 天、pickle 值;Redis 被清空后 cache 后端全员登出而 cached_db 不受影响;maxmemory + allkeys-lru 会把 session 一起淘汰;本机三种后端每请求耗时 0.41 / 0.41 / 0.40 ms(无差异)。
用真实的 manage.py test 演示查询次数测试:assertNumQueries 精确断言与 N+1 失败时列出每条 SQL 的报错;CaptureQueriesContext 上界断言;TestCase 里 atomic 是 SAVEPOINT、TransactionTestCase 里是 BEGIN / COMMIT。
在 admin 列表页上实战 N+1:admin 会自动对 list_display 里的外键做 select_related,但 __str__ 里更深的外键(author.category)仍然每行一次(55 次查询 / 50 行);list_select_related 之后固定 5 次;10/50/200 行的查询数 15/55/105(admin 每页 100 条)。
用 Django Debug Toolbar 的 SQL 面板看 N+1 页面:101 次查询、100 次 similar / 100 次 duplicate,并带调用栈;select_related 后只剩 1 次;同一页面有无工具栏的请求耗时 10.31 ms → 22.24 ms。
遍历 20 万行:list() 与直接 for 峰值内存 155.0 MiB,iterator() 1.7 MiB、chunk_size=500 时 0.5 MiB;PostgreSQL 上同样;iterator() 与 prefetch_related 一起用必须给 chunk_size;iterator() 之后不缓存。
Signal 的基本行为:按连接顺序调用并返回 [(receiver, 返回值)];receiver 必须有 **kwargs;sender 过滤;send() 遇异常直接抛出、send_robust() 捕获并继续;dispatch_uid 去重;一次 send 的开销 0.12–5.61 µs。
用 execute_wrapper 记录 SQL,看 pre_save / post_save / pre_delete / post_delete 与 INSERT、UPDATE、DELETE 的交错顺序;update_fields 会传进信号;atomic 里 post_save 在 COMMIT 之前;queryset.update() 不发信号;pre_save 里改字段会写入。
Signal 去重靠的是函数对象的身份:同一个函数 connect 两次只算一个,但工厂函数造的相同代码、被不同模块名导入两次的 @receiver、每次新建的 lambda 都会重复触发;dispatch_uid 让它们只留一个。
在自己起的 PostgreSQL 上验证:atomic 里 post_save 触发时另一个连接看不到数据,on_commit 时看得到;回滚后 post_save 里的副作用已经发生,on_commit 回调不执行;autocommit 时 post_save 已可见。
把业务逻辑放进 post_save:一行 create() 悄悄变成 7 次查询;receiver 抛异常会让 create() 抛出、无事务时订单已写入;在信号里 save() 自己会 RecursionError;执行顺序就是 connect 顺序;改成显式服务函数则一目了然。
自定义 Signal 的写法与边界(Django 6.1.1):providing_args 已移除;sender 按相等匹配,类、字符串、实例互不相通;send() 与 asend() 对同步/异步 receiver 的耗时;send_robust 把异常放进返回值。