ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python后端中间件专题01:中间件不是“多装几个容器”——请求之外的职责与故障地图

Python后端中间件专题01:中间件不是“多装几个容器”——请求之外的职责与故障地图 Python后端中间件专题01中间件不是“多装几个容器”——请求之外的职责与故障地图周一早上客服群里同时出现三句话“工单提交了但没收到通知”、“搜索不到刚创建的单”、“Redis 超时了”。如果值班者把这三件事统称为“工单系统挂了”他很可能做出错误恢复因为搜索缺文档而重建业务行因为缓存不可用而拒绝所有详情读取或者把通知失败误报成创建失败。第一课不启动任何中间件。我们先学会画故障地图把 PostgreSQL 事实、Redis 加速副本、Elasticsearch 投影和异步通知效果分开并为 Redis、搜索、通知故障各写出不篡改“工单已经提交”的用户契约。阅读下面的图之前确认你能读懂 PythonProtocol/dataclass知道 HTTP 失败会触发重试也理解数据库事务整体提交或回滚。本篇不要求安装 Redis 或 Docker按专题导读建立 Python 3.11 的 01–08 实验工作区后面的 selector 都从project/运行。读图的第一问事实有没有被碰到TicketFlow 只承认 PostgreSQL 里的tickets行是工单事实。一次创建在同一数据库事务中写入工单、幂等收据和 Outbox 事件。只有这次提交成功API 才有资格返回稳定工单 ID。其他组件都是从属者Redis 存了一份详情副本可以过期或被整库清除Elasticsearch 存了方便全文检索的投影可以从 PostgreSQL 重建RabbitMQ 运送待执行的工作ACK 只表示消费者对消息做了处理决定不是新的工单事实通知 sink 是异步副作用的可审计结果它允许比 HTTP 响应晚到日志、指标和 trace 是证据不参与业务裁决。这种分类直接决定降级方式。Redis 宕机时普通详情可以在有界限的前提下回源数据库Elasticsearch 宕机时搜索应清晰返回不可用但创建和按 ID 读取不该被拖下水通知延迟时应查 Outbox、broker 和 worker 的交接不能重新插入工单。把这条判断锁在领域边界里下面的TicketService看起来很薄但它做了重要选择所有业务操作只依赖TicketStore协议。它没有RedisTicketStore、SearchTicketStore或“哪个库先返回就信谁”的分支。数据库 repository 拥有事务缓存与搜索只能包装读路径或消费已提交事件。这份模块中有三个值得留意的约束。第一每个读写方法都显式携带tenant_id租户隔离不是路由层的临时if。第二create要求idempotency_key因为 HTTP 超时后客户端一定会重试。第三IdempotencyConflict是业务冲突不应被吞掉并变成另一次创建。用一条反例验证边界没有倒置本课检查点创建一张 SQL 工单然后在 Redis 写入一个只存在于缓存中的伪造文档。断言不只看 happy path它还确认这个伪造 ID 不会凭空出现在tickets或 Outbox 中。缓存文档还要带由所有 API/Worker 共享部署密钥计算的 HMAC结构完整但没有合法签名的 Redis 文档只能当作 miss不能越过 PostgreSQL 的存在性裁决。运行命令python -m pytest tests/unit/test_architecture_contract.py::test_sql_repository_is_the_only_ticket_truth_and_adapters_are_non_authoritative -q本地 Python 3.11 实际输出为. [100%] 1 passed本地证据仍来自 SQLite、fake Redis 和 HTTP mock。可信远端 selectortest_real_postgres_remains_ticket_truth_when_redis_state_disagrees会在真实 PostgreSQL 中核对 ticket、幂等收据与 Outbox 各一行保留一个可完整反序列化但无合法签名的 Redis-only 文档并要求 HTTP 仍为 404再停 Redis 验证原工单仍可读。未执行远端 selector 前仍是 PENDING。值班分诊时禁止的三种“修复”把“搜索不到”解释为“工单没创建”继而从 Elasticsearch 反写 PostgreSQL这颠倒了事实源与投影。Redis 超时就让所有详情请求失败而没有先判断该端点能否有界回源数据库。HTTP 已返回 201 后因通知延迟再次创建工单正确恢复入口是原工单对应的 Outbox 事件。本课练习把故障地图变成分诊卡。练习 EX-01-01 [code]用一个可执行 Python 表格分别表达三种故障创建已提交但通知延迟、Elasticsearch 宕机、Redis 超时。每项必须给出truth_affected、user_contract和recovery并用断言保证三种故障都不能把已提交工单改写为“未创建”。练习 EX-01-02 [prose]为GET /live和GET /health各写一句契约哪个可以完全不访问外部依赖PostgreSQL、Redis 或搜索故障时哪个状态应决定 API 是否 ready下一课会把这两条契约放到可测试的应用工厂旁边。完成两题后带着“先确认事实是否提交、再处理从属组件”的判断进入第 02 篇。它会逐题回答 EX-01-01/02把健康契约变成可调用的/live、依赖注入式/health和独立 selector并建立 POST→GET→PATCH 的最小基线。完整核心模块工单用例与存储协议Ticket use cases; persistence is delegated to a transaction-owning repository.from__future__importannotationsfromdataclassesimportdataclassfromdatetimeimportdatetimefromtypingimportOptional,ProtocolclassIdempotencyConflict(RuntimeError):Raised when a retry key is reused for a different create request.dataclass(frozenTrue)classTicket:id:strtenant_id:stractor_id:strtitle:strbody:strpriority:strstatus:strversion:intcreated_at:datetime updated_at:datetimeclassTicketStore(Protocol):defcreate_ticket(self,tenant_id:str,actor_id:str,title:str,body:str,priority:str,idempotency_key:str,)-Ticket:...defget_ticket(self,tenant_id:str,ticket_id:str)-Optional[Ticket]:...defupdate_ticket(self,tenant_id:str,ticket_id:str,title:Optional[str]None,body:Optional[str]None,priority:Optional[str]None,)-Optional[Ticket]:...classTicketService:Applies the application contract while keeping tenant-scoped storage explicit.def__init__(self,repository:TicketStore)-None:self._repositoryrepositorydefcreate(self,tenant_id:str,actor_id:str,title:str,body:str,priority:str,idempotency_key:str,)-Ticket:returnself._repository.create_ticket(tenant_id,actor_id,title,body,priority,idempotency_key)defget(self,tenant_id:str,ticket_id:str)-Optional[Ticket]:returnself._repository.get_ticket(tenant_id,ticket_id)defupdate(self,tenant_id:str,ticket_id:str,title:Optional[str]None,body:Optional[str]None,priority:Optional[str]None,)-Optional[Ticket]:returnself._repository.update_ticket(tenant_id,ticket_id,title,body,priority)None,) - Optional[Ticket]:return self._repository.update_ticket(tenant_id, ticket_id, title, body, priority)
返回列表