Skip to content

横切能力与副作用边界 ​

本站纵览 | 练习答案

python
# tickets/middleware.py
import re
import uuid

REQUEST_ID = re.compile(r"^[A-Za-z0-9._-]{1,80}$")


class RequestIdMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        supplied = request.headers.get("X-Request-ID", "")
        request.request_id = supplied if REQUEST_ID.fullmatch(supplied) else uuid.uuid4().hex
        response = self.get_response(request)
        response["X-Request-ID"] = request.request_id
        return response

把它放在需要读取 request_id 的 middleware 之前。若日志上下文使用 context variable,还必须在响应和异常后清理,避免 worker 复用造成串号。

python
# tickets/cache.py
from django.core.cache import cache
from .models import Ticket


def ticket_cache_key(ticket_id, viewer_id):
    return f"ticket:v1:{ticket_id}:viewer:{viewer_id}"


def get_ticket_summary(ticket_id, viewer_id):
    key = ticket_cache_key(ticket_id, viewer_id)
    value = cache.get(key)
    if value is None:
        ticket = Ticket.objects.get(pk=ticket_id, assignee_id=viewer_id)
        value = {"id": ticket.pk, "title": ticket.title, "status": ticket.status}
        cache.set(key, value, timeout=60)
    return value
python
# 在写服务中
from functools import partial
from django.core.cache import cache
from django.db import transaction

transaction.on_commit(partial(cache.delete, ticket_cache_key(ticket.pk, user.pk)))

深水边界 ​

  • 本地内存缓存不在多 worker 间共享,也不适合作为一致性真相。
  • key 必须包含会改变可见性的维度;否则缓存会把一个用户的数据交给另一个用户。
  • 缓存删除失败要靠短 TTL、版本键、重试或变更事件恢复。
  • Session 可能存数据库、缓存、文件或签名 Cookie;签名防篡改,不代表内容保密。
  • signal 适合审计、轻量派生和扩展点。付款、认领、发货等核心流程保持显式。
  • 邮件发送是外部副作用,应在提交后入队;请求内直接发送会放大延迟和失败耦合。

验收 ​

请求 ID 经过字符和长度校验;缓存键包含查看者;事务回滚不触发失效/通知;缓存故障有可说明的降级路径。

练习(先独立完成) ​

  1. 为关闭工单加入缓存失效,演练 cache.delete 失败。
  2. 在测试中覆盖调用方传入恶意超长请求 ID。
  3. 画出数据库提交成功但通知入队失败的恢复方案。

完成后核对答案。

深度加餐:横切能力要有顺序,也要有开关 ​

缓存、信号、审计和请求日志都横跨业务;没有清晰顺序就会互相踩脚。

先做一个可观察实验 ​

python
from django.core.cache import cache
from django.db import transaction

def get_dashboard(user_id):
    key = f'dashboard:{user_id}'
    value = cache.get(key)
    if value is not None:
        return value
    value = build_dashboard(user_id)
    cache.set(key, value, timeout=60)
    return value

def update_and_invalidate(ticket, **changes):
    for key, value in changes.items():
        setattr(ticket, key, value)
    with transaction.atomic():
        ticket.save(update_fields=list(changes))
        transaction.on_commit(lambda: cache.delete(f'dashboard:{ticket.owner_id}'))

预期:缓存只在提交成功后失效;事务回滚不会让读者看到已经撤销的状态。

原理下潜与工程边界 ​

中间件顺序决定 request.user、session、CSRF 和缓存头何时可用。信号适合局部副作用,领域关键流程更适合显式 service;缓存键要含租户/用户边界。

cache.get_or_set 在高并发下仍可能重复计算;信号异常会影响保存;缓存不是事实来源,失效失败时必须能回源数据库。

反例排错路线 ​

给命中、未命中、回源、失效和信号耗时分别打指标;发现陈旧数据时先查提交时序,再查缓存 TTL 和多进程一致性。

练习加码(仍不提供答案) ​

加码:为 dashboard 增加防击穿锁或短暂 stale-while-revalidate,并说明失败时的回源上限。

验收时请补充:正常路径、权限或失败路径、以及一条能证明事务/资源/配置边界没有被绕过的测试。

继续查阅:Django 官方文档(外部资料已收录于证据账本) · Django 安全清单(外部资料已收录于证据账本)

课程合同:Middleware、Session、Cache 与 Signal ​

学习目标 ​

学完本篇,读者应能把「请求 ID 可追踪,用户相关缓存不会被另一用户命中」拆成输入、可信边界、Django 机制和可复查证据;同时能说明 Django 6.0 与 5.2 LTS 的差异不会改变业务不变量。

心智模型 ​

请求/数据输入 → 边界检查 → Django 核心机制 → 可观察输出 → 恢复动作。配置、数据库提交和外部副作用分别拥有自己的生命周期;没有证据的“应该可以”不算完成。

本地机制图 ​

Middleware、Session、Cache 与 Signal机制图

图中的“输入”是不可信的 HTTP、表单、任务或配置;“边界”负责身份、格式、权限、大小、超时与版本;“机制”才是 URL、View、ORM、事务、缓存、Tasks 或 ASGI;“证据”是测试、响应、日志、查询计划和发布记录。

最小实验 ​

在全新虚拟环境中运行:

bash
python -m django --version
python manage.py check
python manage.py test

实验只验证一个机制:输入一条有效值,再输入一条空值、重复值或无权限值。每次实验都记录 Django 版本、数据库、命令和退出码。

完整场景 ​

把实验接到 tickets 工单流:请求携带用户身份和工单 ID,经过当前专题负责的边界,完成一次读或写,并留下响应状态、数据库变化、日志字段和清理动作。完整场景必须能第二次执行,或明确说明幂等键、事务回滚、缓存失效和任务重试策略。

运行输出 ​

成功路径应出现:

text
Django 6.0.x
System check identified no issues (0 silenced).
OK

失败路径要保留真实信号,例如 400(输入无效)、403(权限/CSRF)、404(资源不存在)、405(方法不允许)或数据库约束异常;不要把失败改写成含糊的 200。

失败边界 ​

至少验证:空输入、重复提交、匿名用户、越权对象、并发写入、下游不可用、超时和版本不兼容。记录“谁拒绝、何时拒绝、状态是否回滚、重试是否安全”。开发模式的 traceback 不得进入生产响应,缓存不得跨用户复用敏感结果。

原理与排错 ​

先确认版本和 settings,再用 manage.py check 定位启动问题;接着确认 URL/任务入口、输入解析和权限;最后检查 SQL、事务提交、响应和副作用。若结果异常,用最小复现打开 SQL/请求 ID/任务 ID 观测,沿「复现 → 缩小 → 观测 → 修复 → 回归」闭环,不靠重装依赖猜测。

练习(正文不泄答案) ​

  1. 为上述完整场景补一条正常路径和两条失败路径测试,并记录完整输出。
  2. 画出本篇机制图,标出身份 owner、数据提交点、缓存/任务副作用和恢复动作。
  3. 故意制造一次重复、越权或超时,说明应返回的状态、日志字段和是否可重试。
  4. 写一条 Django 5.2 LTS 兼容验收标准,再说明 Django 6.0 的新能力是否需要条件分支。

验收标准:命令可重复、输出可复查、正文没有练习答案;完成后再打开本阶段答案册。

答案册 ​

本阶段答案册提供参考实现、关键推理、失败边界和替代方案;先独立完成练习再对照。

下一篇 ​

下一篇是07-middleware-sessions-cache/02-email-signals-session-lifecycle,继续沿同一条输入—边界—机制—证据链前进。

用心记录每一步成长