FastAPI 与执行方式:HTTP、同步和异步入门

课前先保留判断:课前预习;课后用你的结果复验:课后复习。

今天从哪里开始

昨天的下单服务只能从 Python 中调用,商城页面和客服工具还无法使用它。今天先把“查询一笔订单”变成 HTTP 请求,明确路径、输入、响应与错误,再接回昨天的数据库服务。

接口能正确响应后,再观察等待:两个独立请求为何有时可以重叠,数据库阻塞调用为什么不能只改成 async 就变快。先解决正确通信,再讨论执行方式。

能力与项目增量

今天要学会什么

  • HTTP 方法与状态码
  • Pydantic 与字段所有权
  • OpenAPI 与 TestClient
  • 同步、阻塞、async、await、并发

课堂结束时,用自己的项目检查:

  • 能做出来:独立完成订单详情;比较顺序 await 与并发调度,并展示正常输入的结果。
  • 能查出问题:针对“在 async def 中直接调用阻塞数据库、把异步当并行或可靠后台任务”中的风险,说明输入、预期与实际差异;失败后核对数据没有被误改。
  • 能解释修改:提交代码、测试和修改前后结果,讲清自己接受或拒绝了哪一处 AI 建议。

从进程内调用走向系统间调用

CLI 与 HTTP 调用对比

看图时注意左、右两侧的边界。

CLI(命令行界面) 调用依赖同一个 Python 工程和进程。

HTTP(超文本传输协议) 调用把能力放在一个稳定入口后面:

  • 调用方只需要知道协议;
  • 服务端可以更换内部实现;
  • 双方用请求与响应验证约定;
  • 后面的浏览器和 AI 客服可以复用同一入口。

这就是今天引入 Web(网页应用) API 的原因。


API 不是“写几个路由”

API 是 Application Programming Interface(接口),中文常说“应用程序编程接口”。

今天我们只抓住一句:

API 是两个程序之间可以被执行和验证的约定。

约定至少包含:

  • 方法:想做什么动作;
  • 路径:操作哪个资源;
  • 输入:允许提交哪些数据;
  • 输出:成功时返回什么;
  • 失败:用什么状态码和结构表达。

少一项,调用方都可能靠猜。


两个客户端为什么应该使用同一份订单接口约定?

统一字段、金额格式、状态和错误含义,避免各客户端重复并分叉业务规则;服务端负责可信校验,OpenAPI 描述接口结构。

FastAPI 第一次使用:先建立可复现环境

FastAPI(Python Web接口框架)、Uvicorn、Pydantic(Python数据校验库)、pytest(Python测试框架) 和 HTTPX 都是 Python 项目依赖,不是 Windows 桌面软件。先领取 W02-D03 日包,在 http_first_example/backend 运行独立 HTTP 小例;随后回到你累计项目接入昨天的数据库服务。两处都使用当前课程虚拟环境。

当前冻结清单:fastapi==0.141.1、uvicorn[standard]==0.41.0、pydantic==2.13.4、pytest==9.1.1、httpx==0.28.1。版本变化必须先更新代码清单并重跑 G0,不能只改课件数字。

py -3.12 -m venv .venv
# 在 TRAE 选择 .venv,关闭旧终端并新开终端
python -c "import sys; print(sys.executable)"
python -m pip install -r requirements.txt
python -m pip check
python -c "import fastapi, uvicorn, pydantic; print(fastapi.__version__, uvicorn.__version__, pydantic.__version__)"

上面安装命令在含当天依赖清单的目录运行;独立 HTTP 小例使用 http_first_example/backend 中的清单,累计项目使用自己的清单,不混用工作目录。解释器必须属于当前 .venv。安装成功后先运行一个最小 /health,再进入商品和订单业务。完整排错见 每周 Python 虚拟环境与依赖安装手册。


一次 HTTP 对话的结构

请求与响应结构

先从请求看:

POST /orders HTTP/1.1
Content-Type: application/json
 
{
  "customer_id": "C1001",
  "items": [
    {"product_id": "P1001", "quantity": 2}
  ]
}
  1. 请求行(起始行):POST /orders HTTP/1.1

    • 请求方法 POST:用于传输实体主体,向服务器提交数据,常用于创建资源。GET 一般用于获取资源,规范上不使用报文主体。
    • 请求 URI /orders:资源定位地址,标识本次操作对应的服务端资源。
    • 协议版本 HTTP/1.1:HTTP/1.1 默认开启持久连接(长连接),同一个 TCP 连接可以多次收发 HTTP 请求,不用每次通信都断开重建 TCP 连接,降低握手开销。
  2. 请求头部(Header):由多组Key: Value键值对构成 示例:Content-Type: application/json

    • Content-Type属于实体首部,作用是描述报文主体的数据格式,告诉服务端:本次报文主体是 JSON 格式文本。
    • HTTP 头部分为 4 大类:
      1. 通用首部:请求、响应报文都可以使用(如Cache-Control、Connection)
      2. 请求首部:仅出现在请求报文(如Accept、User-Agent、Authorization)
      3. 响应首部:仅出现在响应报文
      4. 实体首部:专门描述报文主体(Content-Type、Content-Length)
  3. CRLF 空行(强制要求) 头部区域结束后必须保留空行,用来标记头部结束。服务端依靠这个空行区分头部和报文主体,缺少空行会导致报文解析失败。

  4. 报文主体(Request Body,请求体) 空行之后的 JSON 文本,就是报文实体。POST、PUT 这类方法才会携带报文主体;GET 请求规范不建议携带报文主体。

    核心要点:报文主体本质只是一段纯文本,本身不自带格式信息,数据格式由实体首部Content-Type来声明。

补充核心特性:HTTP 是无状态协议 协议本身不会保存客户端的历史请求信息,不会记住上一次是谁发起的请求。如果需要维持登录会话,需要额外使用 Cookie、Token 等机制,协议原生不具备会话记忆能力。

再从响应看:

HTTP/1.1 201 Created
Content-Type: application/json
 
{
  "order_id": "O20260825001",
  "total_amount_yuan": "258.00",
  "order_status": "PENDING_PAYMENT"
}

响应报文同样由 4 段结构组成:

  1. 状态行(起始行):HTTP/1.1 201 Created
    • 协议版本:HTTP/1.1
    • 状态码 201 + 原因短语 Created 状态码分为 5 大类:
    • 2xx 成功类:请求正常处理完毕。200 OK 是通用成功;201 Created 代表资源创建成功,新建订单场景推荐使用该状态码。
    • 3xx 重定向类:资源位置发生变动,需要客户端重新发起请求。
    • 4xx 客户端错误类:请求参数、权限、地址等客户端侧问题。
    • 5xx 服务端错误类:服务内部代码或系统异常。
  2. 响应头部:Content-Type: application/json 实体首部,告知客户端:本次响应的报文主体是 JSON 文本。
  3. CRLF 空行:分隔头部区域与响应报文主体。
  4. 响应报文主体(Response Body,响应体) JSON 字符串,承载服务端返回的业务结果:订单编号、订单总金额、订单状态。

核心概念区分

  1. 报文、实体、报文主体
  • 实体:请求 / 响应中传输的完整数据单元,包含实体首部 + 实体主体。
  • 报文主体:实体里面承载的原始数据,也就是示例里的 JSON 字符串。 简单理解:实体是完整数据包,报文主体是数据包里面存放的数据内容。
  1. 内容协商 请求头可增加Accept: application/json,表达客户端期望服务端返回 JSON 格式的数据。服务端读取 Accept 头部,选择合适的数据格式返回,这个机制称为内容协商。
  2. Content-Length 真实线上请求、响应中,一般会自动携带Content-Length,用来记录报文主体的字节长度。客户端和服务端依靠这个字段,判断报文主体读到哪里为止。

特殊场景使用Transfer-Encoding: chunked分块传输,此时不需要 Content-Length,适合动态生成、长度不确定的返回内容。

  1. 底层传输关系 本课 HTTP/1.1 属于应用层协议,通常运行在 TCP 上;HTTP/3 使用 QUIC,不能将 TCP 概括为所有 HTTP 版本的唯一基础。TCP 负责将 HTTP 报文拆分成网络数据包,保证数据可靠、有序送达;HTTP 只定义报文的格式和业务语义,不负责网络传输。

常见认知误区

  1. ❌ 误区:POST 请求比 GET 更安全

纠正:POST 只是把数据放在报文主体,GET 将参数放在 URL 查询串。HTTP 本身传输全部是明文,POST 本身没有加密能力;想要防止网络窃听,需要使用 HTTPS,在 TCP 和 HTTP 之间增加 SSL/TLS 加密层。

  1. ❌ 误区:只要传入 JSON,服务端就能自动识别

纠正:Content-Type实体首部是给服务端的解析说明。调用方应按合同声明 JSON 类型;缺失或错误时,框架可能推断、拒绝或按其他格式处理,不能依赖猜测。实际以本课接口测试结果为准。

  1. ❌ 误区:响应报文必须携带响应体

纠正:部分状态码(例如 204 No Content)的响应可以不带报文主体。201 Created 允许带或者不带主体,业务场景里一般会返回新建资源的信息。

查阅:TCP 三次握手、四次挥手

本节按需查阅;课堂主线继续到“方法、路径和资源”,保留时间完成商城接口和异步取消实验。

HTTP 是应用层协议,它建立通信前,需要先由传输层 TCP 协议建立连接;通信结束后,TCP 负责断开连接。三次握手、四次挥手属于 TCP 的机制,是 HTTP 相关面试 / 笔试高频考点。

一、TCP 三次握手:建立连接(HTTP 通信的前置步骤)

HTTP/1.1 基于 TCP,在发送 HTTP 请求报文之前,客户端和服务器必须先通过三次握手建立可靠 TCP 连接。

  1. 第一次握手(SYN):客户端发送 SYN 报文。客户端告诉服务端:我想建立连接,我的序列号是 seq=x。
  2. 第二次握手(SYN+ACK):服务端收到 SYN,回复 SYN+ACK 报文。含义:收到你的连接请求;我这边同意建立连接,我的序列号 seq=y,确认号 ack=x+1。
  3. 第三次握手(ACK):客户端收到 SYN+ACK,回复 ACK 报文。确认号 ack=y+1。服务端收到这个 ACK 后,TCP 连接正式建立。

✅ 一句话总结目的:双方互相确认「自己的发送能力、对方的接收能力都正常」。

不是三次握手就开始传 HTTP 数据:三次握手完成之后,才会发送我们上面例子里的 POST /orders HTTP 请求报文。

补充:HTTP/1.1 长连接(keep-alive),就是这个 TCP 连接建立后,可以连续传输多组 HTTP 请求 - 响应,不用每次 HTTP 请求都重新握手。

二、TCP 四次挥手:断开 TCP 连接(HTTP 通信结束之后)

TCP 是全双工通道(两边可以同时收发数据),双方都可以单独关闭自己的发送通道,所以断开需要四次交互。

  1. 第一次挥手(FIN):客户端发送 FIN 报文,表示:客户端不再发送新数据,但还可以继续接收服务端剩下的数据。
  2. 第二次挥手(ACK):服务端回复 ACK,确认收到关闭请求。此时半关闭状态:客户端不再发数据,服务端还能继续给客户端传剩余数据。
  3. 第三次挥手(FIN):等服务端把剩余数据全部发送完毕,服务端发送 FIN 报文,告诉客户端:我这边也不再发送数据了。
  4. 第四次挥手(ACK):客户端回复 ACK 确认。服务端收到 ACK,连接正式关闭。

常考点:为什么建立连接是 3 次,断开却要 4 次? 核心原因:TCP 全双工。建立连接时 SYN 和 ACK 可以合并在同一个报文;关闭连接时,服务端收到 FIN 之后,可能还有业务数据没传输完,不能立刻发 FIN,典型时序中 ACK 与 FIN 分开发送,形成四次交互;具体报文可能合并,不把四次当作所有抓包都必须出现的固定数量。


方法、路径和资源

GET 通常用于读取资源。

POST 通常用于创建资源或发起一个非幂等动作。

今天先使用这两种方法。

路径中的名词代表资源:

  • /products:商品集合;
  • /products/P1001:一个商品;
  • /orders:订单集合;
  • /orders/O20260825001:一个订单。

这里不追求“REST(表述性状态转移接口风格) 风格大全”。

我们只要求路径能稳定表达本周真实业务对象。


查订单和创建订单的 HTTP 方法、路径及输入位置应该如何区分?

查询通常用 GET 加资源路径或查询参数,创建用 POST 加请求体;按接口合同选择状态码和错误结构。HTTP 成功响应还要核对业务数据,不能只看网络是否连通。

Path、Query 和 Body 放什么

Path parameter(路径参数)用来定位某个资源,不是操作系统的 PATH 环境变量:

/products/{product_id}

Query(查询) parameter 用来改变查询视角:

/products?category=keyboard&active_only=true

Request body 用来提交结构化意图:

{
  "customer_id": "C1001",
  "items": [{"product_id": "P1001", "quantity": 2}]
}

如果把所有内容都塞进一个字符串,调用方和服务端都会失去清晰边界。


OpenAPI 在这里做什么

OpenAPI(接口描述规范) 是一份机器可读的 API 描述。

它可以描述:

  • 路径与方法;
  • 参数位置;
  • 请求与响应 Schema(数据结构约束);
  • 状态码;
  • 错误结构。

Schema 可以理解为数据结构和约束的合同。

Swagger UI 是查看、试调用 OpenAPI 的界面。

两者不能画等号。

页面能打开,不代表行为符合合同。


三份信息必须互相对齐

OpenAPI 到运行行为的映射

今天每实现一个端点,都做三次核对:

  1. OpenAPI 写了什么;
  2. FastAPI / Pydantic 实现了什么;
  3. 真实请求实际返回什么。

如果只有中间一项,就只是“我觉得代码对”。

如果只有左边一项,就只是“文档写得对”。

如果只看右边一个成功请求,失败合同仍然没有证明。


FastAPI 的最小入口

浏览器访问 /health 时,框架怎么知道要运行哪一个函数?需要先登记“请求方法 + 路径 → 处理函数”的对应关系。

@app.get("/health") 叫路由装饰器:写在函数定义上方,把下面的函数登记为 GET 请求的处理函数。模块加载时完成登记;收到匹配请求时,框架才调用函数处理请求。get_health 是 Python 函数名,/health 是客户端访问路径,两者不必同名。

下面可以保存成独立 health_demo.py,不要覆盖周项目入口。安装本课依赖后执行 python -m uvicorn health_demo:app --port 8011,浏览器访问 http://127.0.0.1:8011/health。冒号前是模块名,冒号后是应用对象名;按 Ctrl+C 停止服务。

订单 API 的运行、调试、测试、文档与生产关系|700

先沿蓝色路径看一次真实 HTTP 请求,再比较调试器、TestClient 和 OpenAPI 分别从哪里进入。它们都服务同一个应用,却能证明不同问题;测试进程通过不能代替真实端口或容器验收。

from fastapi import FastAPI
 
app = FastAPI(title="Teaching Commerce API")
 
 
@app.get("/health")
def get_health() -> dict[str, str]:
    """返回服务存活状态和合同版本。"""
    return {
        "status": "ok",
        "contract_version": "commerce-api-v2",
    }

先读,不要马上复制。

请指出:

  • 哪一行建立应用对象;
  • 哪一行把函数和路径连接;
  • 返回值最后会怎样送给调用方。

为什么今天用同步函数

FastAPI 同时支持普通 def 和 async def。

W02 基础接口采用同步路径和同步 SQLAlchemy(Python数据库工具库) Session(会话)。

原因很具体:

  • 今天先完成 HTTP 合同;
  • 继续复用 W02-D02 已建立的同步 Session 与事务边界;
  • 同时引入协程、异步驱动和连接池会掩盖主线。

这不表示同步永远优于异步。

本周讲完协程、调度、超时和取消;异步数据库驱动迁移不是今天的必做任务。后续应用这些已学机制。


用 Pydantic 表达输入

路由已经能接到请求,但客户端可以发来任意字段和类型。先在入口明确允许什么,再调用昨天的服务,不能让客户端自己决定订单金额或状态。

from pydantic import BaseModel, ConfigDict, Field
 
 
class OrderItemCreate(BaseModel):
    model_config = ConfigDict(extra="forbid")
 
    product_id: str = Field(pattern=r"^P[0-9]{4}$")
    quantity: int = Field(ge=1, strict=True)
 
 
class OrderCreate(BaseModel):
    model_config = ConfigDict(extra="forbid")
 
    customer_id: str = Field(pattern=r"^C[0-9]{4}$")
    items: list[OrderItemCreate] = Field(min_length=1)

你猜:

  • quantity 为 0 会发生什么;
  • 多传一个 total_amount_yuan 会发生什么;
  • product_id 写成 1 会发生什么。

先预测,再运行。

POST orders 从 HTTP 请求到 Pydantic 校验、业务数据和响应的时序|700

图中校验失败会在进入业务保存前返回 422。顺着箭头解释一次:哪个阶段拒绝无效结构,哪个阶段生成订单事实,为什么响应模型不能由客户端提交。


“写了 int”就能挡住所有错误类型吗?

先猜这三个请求会不会通过。Pydantic 默认可能做类型转换;订单数量需要按 JSON(结构化数据交换格式) 合同严格检查。

请求里的 quantity业务期望为什么
2通过正整数
true422,库存不变布尔值不能当作购买一件
"2" 或 2.5422,库存不变字符串/小数不符合整数合同

项目模型使用 quantity: int = Field(ge=1, strict=True)。先运行请求,再查看库存是否保持原样。这延续了 W01/W02 的数量规则,换了框架也不能把边界丢掉。

输入模型和输出模型为什么分开

输入模型表达“客户端可以提出什么”。

输出模型表达“服务端承诺返回什么事实”。

两者字段不应该完全相同。

创建订单的输入里没有:

  • order_id
  • unit_price_yuan
  • line_total_yuan
  • total_amount_yuan
  • order_status

这些字段都要由服务端生成。


字段所有权是一条信任边界

服务端字段边界

请看左侧:客户端表达购买意图。

再看右侧:服务端查商品、查库存、计算金额并生成状态。

如果客户端提交:

{
  "customer_id": "C1001",
  "items": [{"product_id": "P1001", "quantity": 2}],
  "total_amount_yuan": "0.01"
}

服务端不能把 0.01 元当成订单事实。

今天我们选择直接拒绝未知字段。


为什么金额是两位小数字符串

中国大陆的业务展示以“元”为单位。

API 示例:

{
  "unit_price_yuan": "129.00",
  "line_total_yuan": "258.00"
}

两位小数字符串让消费者明确看到精度。

Python 内部使用 Decimal。

W02-D01 已在 MySQL 使用 DECIMAL(10,2);这里保持数据库、Python 与 JSON 响应之间的金额精度约定。

不要把金额换成 float。


状态码不是随便挑的数字

状态码决策

201 Created:资源已经创建。

404 Not Found:合同中定位的资源不存在。

422 Unprocessable Content:请求结构或字段约束不成立。

409 Conflict:请求结构正确,但和当前业务事实冲突。

库存不足或商品下架属于 409。

数量为 0 属于 422。


统一错误结构

失败响应统一为:

{
  "code": "RESOURCE_NOT_FOUND",
  "message": "product P9999 not found",
  "request_id": "req-..."
}

code 给程序判断。

message 给人阅读。

request_id 用来关联本次请求和日志。

今天教师提供受限错误适配脚手架。

我们会接入、运行反例并解释,不把中间件设计展开成另一门课。


request_id 为什么有用

前端说:“创建订单失败了。”

后端日志里同一秒可能有几十条请求。

如果响应带着 request_id,前端就能把它交给后端:

request_id=req-2f76...

后端用同一个 ID 查日志。

它不是用户身份,也不代表鉴权。

它只是一次请求的排错关联标识,后续用于链路追踪,常用框架有SkyWalking。


用真实请求验证

这里要区分刚才的独立健康检查示例和本周数据库接口。前者只认识 HTTP;后者才调用昨天的业务服务。

配套基础服务从后端目录启动,入口为 app.foundation:app,不是 W03 带 Redis 与员工权限的 app.main:app。学生累计项目若另取模块名,需在自己的 README 写明对应入口。

python -m uvicorn app.foundation:app --host 127.0.0.1 --port 8001

先访问 http://127.0.0.1:8001/health,再打开 http://127.0.0.1:8001/docs 发出商品查询、订单创建和详情请求。核对状态码、响应字段及数据库变化;只打开文档页还没有验证业务。

本周基础入口只用于本地学习,不暴露到公网。W03 再接入员工会话与权限。

同步、异步、并发、并行分别在回答什么

现在请求已经走通,接下来才讨论等待方式。沿着同一个请求画出“执行 Python → 等外部结果 → 继续处理”,看哪些时间真的能够重叠。

我先区分四个问题:调用方是否等待结果;等待时是否占住当前执行线程;多个任务能否交替推进;是否真的同时使用多个计算资源。同步调用与阻塞经常一起出现,但它们描述的角度不同。

概念当前例子判断方式
同步调用查询商品返回后,再查询配送说明第二步在第一步返回后开始
阻塞等待同步网络或数据库函数等待结果当前执行线程不能继续这段代码
异步等待协程在 await 处把执行机会交回事件循环其他已调度任务可以推进
并发两个 I/O 任务的等待区间重叠任务生命周期重叠
并行多个计算单元同时计算需要实际执行资源,不能只看 async 关键字

先认识协程,再运行等待对照

进程(Process):程序的一次运行实例,拥有自己的内存和资源。 线程(Thread):进程内的一条执行路径,同一进程的线程共享部分资源。 协程(Coroutine):用户态、代码层面实现的轻量 “子程序”,不由操作系统内核调度。

async def 定义协程函数;调用它得到协程对象,函数不会因此自动完整执行。await 等待可等待对象完成,调用方在需要结果的位置继续。asyncio.run() 负责这个独立脚本的事件循环入口。只有同步函数基础讲清后,才运行下面的例子。

import asyncio
 
async def fetch_label(label):
    await asyncio.sleep(0.05)  # 用本地等待模拟 I/O,不是真实网络
    return label
 
async def main():
    first = await fetch_label("商品")
    second = await fetch_label("配送说明")
    print("顺序等待", first, second)
    values = await asyncio.gather(fetch_label("商品"), fetch_label("配送说明"))
    print("并发等待", values)
 
asyncio.run(main())

连续写两个 await 仍然顺序执行;gather 把两个可等待任务一起调度,并按传入顺序汇总结果。它适合相互独立的 I/O 等待。不能把“先查库存再决定下单”这样的依赖步骤随意并发。

在公共示例包运行 python demos/sync_async.py。先解释 active 和 peak 记录什么,再看顺序与并发的输出。示例用等待模拟外部服务,其耗时不能用来宣称真实模型或数据库提速。

连续两次 await 是并发吗?

不一定。先 await A 再 await B 时,B 要等 A 完成才开始;把独立协程交给 gather 等调度方式后,等待阶段才可重叠。异步不保证 CPU 多核并行。

商品详情需要两个结果时,怎样等、怎样停

为什么学:商城商品详情可能同时需要商品资料和配送说明。如果两项相互独立,等待可以重叠;如果配送服务一直不回答,就不能让页面永远转圈。今天在本人 commerce_ops/detail_query.py 完成 build_detail(product_id, lookup, timeout)。

lookup(kind, product_id) 是提供的异步连接器接口;课堂替身模拟 product 和 delivery,不返回真实物流信息,也不会更新库存。成功返回包含 product_id/product/delivery 的字典,失败继续报告。真实订单 API 仍接昨天的同步数据库服务。

coroutine、Task 与 gather 的分工

调用 async def 函数得到协程对象;asyncio.create_task(协程) 把它安排为 Task;await 等待结果;gather 汇总多个任务,结果按传入顺序对应,不是按谁先完成对应。

import asyncio
 
async def lookup(kind):
    await asyncio.sleep(0.01)
    return kind
 
async def main():
    first = asyncio.create_task(lookup("product"))
    second = asyncio.create_task(lookup("delivery"))
    print(await asyncio.gather(first, second))
 
asyncio.run(main())

这是可以独立运行的小例,输出 ['product', 'delivery']。Web 框架已经管理事件循环时,不在路由里再调用 asyncio.run();直接 await 自己的协程。

超时以后,还要等自己的任务完成清理

async with asyncio.timeout(秒数) 为一段等待设置期限;超时在退出范围后表现为 TimeoutError。task.cancel() 发出取消请求,不保证取消瞬间已经清理完毕。协程应在 finally 中归还资源,清理后不要吞掉 CancelledError。

import asyncio
 
async def slow_lookup():
    try:
        await asyncio.sleep(10)
    finally:
        print("连接器清理完成")
 
async def check_timeout():
    try:
        async with asyncio.timeout(0.01):
            await slow_lookup()
    except TimeoutError:
        print("本次查询超时")
 
asyncio.run(check_timeout())

先看清理消息,再看到超时消息。本日聚合器还要处理“一项已失败,另一项仍在等”:普通 gather 默认传播第一个异常,但不会因此自动取消全部其他任务。我需要保存自己创建的 Task,在 finally 取消未结束任务,再等待它们清理完成。

清理时使用 gather(..., return_exceptions=True) 接收取消结果;这不是把业务失败改成成功的理由。不要把字典里的异常对象包装成“配送成功”。这是本日必须跑的边界,不留到后面 AI 课程首次讲。

我怎样证明等待真的重叠

python check_d03_python.py

检查器记录同一时刻活动任务数量:普通顺序 await 的峰值为 1;本次两个独立查询的峰值应为 2;成功、超时、失败退出后活动数都应归零。耗时受电脑负载影响,不用“必须快两倍”作为通过门槛。

我独立把配送改成慢响应,再把商品查询改成不存在,观察输出和清理。这个实验能证明本地调度和合同,不能证明真实物流可达、数据库提速,或超时的远端写入已经撤销。

阻塞边界继续遵守:同步 PyMySQL 不直接放入本异步连接器。先把整个同步用例放在现有普通 def 路由;W03 再讲线程池的具体使用,不把同一 Session 跨任务共享。

页面超时后直接重试一次下单,会给业务带来什么风险?

第一次请求可能已写入,重试可能造成重复订单。先核对请求和业务状态,生产方案再结合幂等;超时本身不能证明未发生写入。

超时返回是否说明任务已经成功或完全没有副作用?

都不能。超时只说明调用者等待达到限制;本地协程取消还要执行清理,远端可能已经处理。业务写入要核对状态,不能无条件重试造成重复。

FastAPI 里为什么保留同步数据库路由

本项目使用同步 PyMySQL / SQLAlchemy,所以数据库路由继续用普通 def。FastAPI 会在线程池中执行普通同步路径操作。直接在 async def 中调用同步阻塞函数,不会自动把它变成异步,可能占住事件循环。若以后迁移异步数据库,需要对应驱动、会话与调用链一起调整。

普通函数被业务代码直接调用时,框架也不会自动把每次调用转移到线程池。先识别调用发生在哪一层,再选择方案。

把同步数据库调用放进 async def 就能提速吗?

不能。PyMySQL 的阻塞调用会阻塞该事件循环。本课普通 def 路由由框架在线程池执行;异步驱动迁移要配合完整调用链,不能只改函数声明。

一个请求内的异步,不等于可靠后台任务

asyncio.create_task()、进程内队列或 Web 框架后台回调都依赖当前应用进程。进程退出后,任务是否完成没有持久化保证。订单通知如果需要在应用重启后继续执行,就要考虑跨进程任务系统和消息队列;W03 会用独立生产者与消费者演示。

参考:Python 协程与任务、FastAPI 同步与异步。

学生自己动手,AI 只能改你允许改的地方

我先保存当前可运行版本,再完成“独立完成订单详情;比较顺序 await 与并发调度”。让 AI 一次只修改负责该行为的文件;修改前预测正常与失败结果,检查差异,再运行回归。每人需要解释一处接受或拒绝的建议。

错误与边界怎样验收

今天重点检查:在 async def 中直接调用阻塞数据库、把异步当并行或可靠后台任务。保留输入、预期、实际输出和修复后的结果,不能只贴全绿截图。未验证的外部依赖如实记为待验证。

留给下一阶段

今天把业务服务开放成可验证的接口,并区分了调用顺序与等待方式。下一天代码不换规则,换一台运行环境,看看依赖、端口和配置有没有写清楚。

官方资料