FastAPI 与执行方式:HTTP、同步和异步入门
今天从哪里开始
昨天的下单服务只能从 Python 中调用,商城页面和客服工具还无法使用它。今天先把“查询一笔订单”变成 HTTP 请求,明确路径、输入、响应与错误,再接回昨天的数据库服务。
接口能正确响应后,再观察等待:两个独立请求为何有时可以重叠,数据库阻塞调用为什么不能只改成 async 就变快。先解决正确通信,再讨论执行方式。
今天要学会什么
- HTTP 方法与状态码
- Pydantic 与字段所有权
- OpenAPI 与 TestClient
- 同步、阻塞、async、await、并发
课堂结束时,用自己的项目检查:
- 能做出来:独立完成订单详情;比较顺序 await 与并发调度,并展示正常输入的结果。
- 能查出问题:针对“在 async def 中直接调用阻塞数据库、把异步当并行或可靠后台任务”中的风险,说明输入、预期与实际差异;失败后核对数据没有被误改。
- 能解释修改:提交代码、测试和修改前后结果,讲清自己接受或拒绝了哪一处 AI 建议。
从进程内调用走向系统间调用
看图时注意左、右两侧的边界。
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}
]
}-
请求行(起始行):
POST /orders HTTP/1.1- 请求方法
POST:用于传输实体主体,向服务器提交数据,常用于创建资源。GET 一般用于获取资源,规范上不使用报文主体。 - 请求 URI
/orders:资源定位地址,标识本次操作对应的服务端资源。 - 协议版本
HTTP/1.1:HTTP/1.1 默认开启持久连接(长连接),同一个 TCP 连接可以多次收发 HTTP 请求,不用每次通信都断开重建 TCP 连接,降低握手开销。
- 请求方法
-
请求头部(Header):由多组
Key: Value键值对构成 示例:Content-Type: application/jsonContent-Type属于实体首部,作用是描述报文主体的数据格式,告诉服务端:本次报文主体是 JSON 格式文本。- HTTP 头部分为 4 大类:
- 通用首部:请求、响应报文都可以使用(如
Cache-Control、Connection) - 请求首部:仅出现在请求报文(如
Accept、User-Agent、Authorization) - 响应首部:仅出现在响应报文
- 实体首部:专门描述报文主体(
Content-Type、Content-Length)
- 通用首部:请求、响应报文都可以使用(如
-
CRLF 空行(强制要求) 头部区域结束后必须保留空行,用来标记头部结束。服务端依靠这个空行区分头部和报文主体,缺少空行会导致报文解析失败。
-
报文主体(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 段结构组成:
- 状态行(起始行):
HTTP/1.1 201 Created- 协议版本:
HTTP/1.1 - 状态码
201+ 原因短语Created状态码分为 5 大类: - 2xx 成功类:请求正常处理完毕。200 OK 是通用成功;201 Created 代表资源创建成功,新建订单场景推荐使用该状态码。
- 3xx 重定向类:资源位置发生变动,需要客户端重新发起请求。
- 4xx 客户端错误类:请求参数、权限、地址等客户端侧问题。
- 5xx 服务端错误类:服务内部代码或系统异常。
- 协议版本:
- 响应头部:
Content-Type: application/json实体首部,告知客户端:本次响应的报文主体是 JSON 文本。 - CRLF 空行:分隔头部区域与响应报文主体。
- 响应报文主体(Response Body,响应体) JSON 字符串,承载服务端返回的业务结果:订单编号、订单总金额、订单状态。
核心概念区分
- 报文、实体、报文主体
- 实体:请求 / 响应中传输的完整数据单元,包含实体首部 + 实体主体。
- 报文主体:实体里面承载的原始数据,也就是示例里的 JSON 字符串。 简单理解:实体是完整数据包,报文主体是数据包里面存放的数据内容。
- 内容协商 请求头可增加
Accept: application/json,表达客户端期望服务端返回 JSON 格式的数据。服务端读取 Accept 头部,选择合适的数据格式返回,这个机制称为内容协商。 - Content-Length 真实线上请求、响应中,一般会自动携带
Content-Length,用来记录报文主体的字节长度。客户端和服务端依靠这个字段,判断报文主体读到哪里为止。
特殊场景使用
Transfer-Encoding: chunked分块传输,此时不需要 Content-Length,适合动态生成、长度不确定的返回内容。
- 底层传输关系 本课 HTTP/1.1 属于应用层协议,通常运行在 TCP 上;HTTP/3 使用 QUIC,不能将 TCP 概括为所有 HTTP 版本的唯一基础。TCP 负责将 HTTP 报文拆分成网络数据包,保证数据可靠、有序送达;HTTP 只定义报文的格式和业务语义,不负责网络传输。
常见认知误区
- ❌ 误区:POST 请求比 GET 更安全
纠正:POST 只是把数据放在报文主体,GET 将参数放在 URL 查询串。HTTP 本身传输全部是明文,POST 本身没有加密能力;想要防止网络窃听,需要使用 HTTPS,在 TCP 和 HTTP 之间增加 SSL/TLS 加密层。
- ❌ 误区:只要传入 JSON,服务端就能自动识别
纠正:
Content-Type实体首部是给服务端的解析说明。调用方应按合同声明 JSON 类型;缺失或错误时,框架可能推断、拒绝或按其他格式处理,不能依赖猜测。实际以本课接口测试结果为准。
- ❌ 误区:响应报文必须携带响应体
纠正:部分状态码(例如 204 No Content)的响应可以不带报文主体。201 Created 允许带或者不带主体,业务场景里一般会返回新建资源的信息。
查阅:TCP 三次握手、四次挥手
本节按需查阅;课堂主线继续到“方法、路径和资源”,保留时间完成商城接口和异步取消实验。
HTTP 是应用层协议,它建立通信前,需要先由传输层 TCP 协议建立连接;通信结束后,TCP 负责断开连接。三次握手、四次挥手属于 TCP 的机制,是 HTTP 相关面试 / 笔试高频考点。
一、TCP 三次握手:建立连接(HTTP 通信的前置步骤)
HTTP/1.1 基于 TCP,在发送 HTTP 请求报文之前,客户端和服务器必须先通过三次握手建立可靠 TCP 连接。
- 第一次握手(SYN):客户端发送 SYN 报文。客户端告诉服务端:我想建立连接,我的序列号是 seq=x。
- 第二次握手(SYN+ACK):服务端收到 SYN,回复 SYN+ACK 报文。含义:收到你的连接请求;我这边同意建立连接,我的序列号 seq=y,确认号 ack=x+1。
- 第三次握手(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 是全双工通道(两边可以同时收发数据),双方都可以单独关闭自己的发送通道,所以断开需要四次交互。
- 第一次挥手(FIN):客户端发送 FIN 报文,表示:客户端不再发送新数据,但还可以继续接收服务端剩下的数据。
- 第二次挥手(ACK):服务端回复 ACK,确认收到关闭请求。此时半关闭状态:客户端不再发数据,服务端还能继续给客户端传剩余数据。
- 第三次挥手(FIN):等服务端把剩余数据全部发送完毕,服务端发送 FIN 报文,告诉客户端:我这边也不再发送数据了。
- 第四次挥手(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=trueRequest body 用来提交结构化意图:
{
"customer_id": "C1001",
"items": [{"product_id": "P1001", "quantity": 2}]
}如果把所有内容都塞进一个字符串,调用方和服务端都会失去清晰边界。
OpenAPI 在这里做什么
OpenAPI(接口描述规范) 是一份机器可读的 API 描述。
它可以描述:
- 路径与方法;
- 参数位置;
- 请求与响应 Schema(数据结构约束);
- 状态码;
- 错误结构。
Schema 可以理解为数据结构和约束的合同。
Swagger UI 是查看、试调用 OpenAPI 的界面。
两者不能画等号。
页面能打开,不代表行为符合合同。
三份信息必须互相对齐
今天每实现一个端点,都做三次核对:
- OpenAPI 写了什么;
- FastAPI / Pydantic 实现了什么;
- 真实请求实际返回什么。
如果只有中间一项,就只是“我觉得代码对”。
如果只有左边一项,就只是“文档写得对”。
如果只看右边一个成功请求,失败合同仍然没有证明。
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 停止服务。
先沿蓝色路径看一次真实 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 会发生什么。
先预测,再运行。
图中校验失败会在进入业务保存前返回 422。顺着箭头解释一次:哪个阶段拒绝无效结构,哪个阶段生成订单事实,为什么响应模型不能由客户端提交。
“写了 int”就能挡住所有错误类型吗?
先猜这三个请求会不会通过。Pydantic 默认可能做类型转换;订单数量需要按 JSON(结构化数据交换格式) 合同严格检查。
| 请求里的 quantity | 业务期望 | 为什么 |
|---|---|---|
2 | 通过 | 正整数 |
true | 422,库存不变 | 布尔值不能当作购买一件 |
"2" 或 2.5 | 422,库存不变 | 字符串/小数不符合整数合同 |
项目模型使用 quantity: int = Field(ge=1, strict=True)。先运行请求,再查看库存是否保持原样。这延续了 W01/W02 的数量规则,换了框架也不能把边界丢掉。
输入模型和输出模型为什么分开
输入模型表达“客户端可以提出什么”。
输出模型表达“服务端承诺返回什么事实”。
两者字段不应该完全相同。
创建订单的输入里没有:
order_idunit_price_yuanline_total_yuantotal_amount_yuanorder_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 中直接调用阻塞数据库、把异步当并行或可靠后台任务。保留输入、预期、实际输出和修复后的结果,不能只贴全绿截图。未验证的外部依赖如实记为待验证。
留给下一阶段
今天把业务服务开放成可验证的接口,并区分了调用顺序与等待方式。下一天代码不换规则,换一台运行环境,看看依赖、端口和配置有没有写清楚。