
这段时间FastAPI在Python后端圈子里的热度一直很高我自己也在几个项目里陆续从Flask和Django迁到了FastAPI整体体验相当不错。今天不聊泛泛的框架介绍就从一个能落地的FastAPI项目出发把选型理由、项目结构、CORS配置、SQLAlchemy集成、纯REST接口实现到常见坑位完整走一遍。无论你是刚接触FastAPI的新手还是准备把老项目迁过来的同学这篇文章都值得认真过一遍。FastAPI解决的核心问题其实很直接用Python快速写出高性能的REST接口同时利用类型注解把参数校验、数据序列化、接口文档自动生成这些脏活全干了。适合用来做前后端分离项目的API层、微服务网关、内部工具平台以及任何对开发效率和接口规范性有要求的场景。接下来说的全部内容都是我实际项目中验证过的方案不是网上抄来的demo。1. 为什么是FastAPI而不是Flask/Django1.1 性能到底快在哪很多人听到FastAPI第一反应是性能好但具体好在哪里得拆开看。FastAPI底层是StarletteStarlette底层又是uvicorn这个ASGI服务器。ASGI和传统WSGI的区别在于它是异步的uvicorn还额外集成了uvloop和httptools这两件套能显著提升事件循环和HTTP解析的效率。打个简单比方传统的Flask开发模式每个请求进来就像是餐厅里一个服务员全程跟着一桌客人客人多了就得不断加服务员线程/进程服务员一多互相抢道开销就上来了。而FastAPI的异步模型更像一个高效的前台调度员一个人同时对接多桌客人的点单需求遇到需要等待的环节比如查数据库就先接下一单等结果回来了再回头处理人数不用加太多餐厅吞吐量照样高。但要注意这种性能优势主要体现在IO密集场景比如大量数据库查询、外部API调用、读写缓存。如果你的接口是CPU密集型比如图像处理、复杂计算那async帮不了太多该用Celery队列还是得用。1.2 类型注解带来的开发体验飞跃这是我个人最看重的一点。FastAPI把所有东西都建立在Python类型注解之上请求参数、请求体、响应模型全部用类型声明。Pydantic在后台完成数据校验和序列化校验失败自动返回422错误字段缺失、类型不对、范围超限都给你写得明明白白。举个例子一个创建图书的接口前端传了{title: Python实战, price: abc}Pydantic会直接告诉你price字段期望float拿到的是str根本不需要自己在代码里写一堆if判断。省下的这部分工作量在接口多的时候是非常可观的。更妙的是类型声明同时驱动了多个环节校验逻辑是它接口文档是它IDE自动补全也是它。改一个字段类型IDE里所有用到的地方立刻标红这种重构信心在Flask那种自由度极高的项目里是感受不到的。1.3 自动化交互式文档FastAPI会自动生成OpenAPI规范然后基于这个规范给你两个现成的文档页面/docs用的是Swagger UI/redoc用的是ReDoc。打开http://127.0.0.1:8000/docs你能看到所有接口的请求参数、请求体示例、响应结构还能直接在页面上点Try it out发起真实请求。这个能力在后端交付场景里特别实用。以前用Flask的时候每次联调都靠Postman导来导去或者手写Markdown文档改一个字段到处同步。FastAPI的文档是代码自带的代码改了文档立刻变永远不会出现代码和文档不一致这种经典扯皮问题。1.4 什么时候不建议用FastAPIFastAPI不是万能的踩过坑之后我也有清醒的认知。如果项目是一个高度依赖服务端模板渲染的站点或者需要一个自带admin后台、ORM、迁移工具全家桶的管理系统Django依然是更稳妥的选择。如果团队里大部分人都不熟悉异步编程让他们强制执行async风格反而会写出比同步更慢的代码。选型要结合团队实际情况框架再优秀团队成员学不会、用不好也是白搭。2. 环境准备与项目目录结构2.1 Python版本与安装FastAPI要求Python 3.8以上但我个人建议直接用3.11或3.12从3.11开始Python引入了大量性能优化跑同样的代码整体会快一截。装好Python之后建议每个项目都建独立虚拟环境不要图省事装到全局。python -m venv venv source venv/bin/activate # Windows下执行 venv\Scripts\activate pip install fastapi uvicorn[standard]这里有个细节uvicorn[standard]一定要带[standard]这部分。standard模式会额外安装uvloop和httptools性能提升非常明显实测同样的接口QPS能差出一个量级。生产环境部署的时候这个细节很容易被忽略很多人装成纯uvicorn就上了性能白丢一大截。2.2 可维护的项目目录结构热词里有人专门搜fastapi项目目录结构说明这个问题确实困扰了不少人。FastAPI没有强制规定目录长什么样但项目一大结构混乱带来的维护成本是实打实的。下面是我个人经过几个项目迭代后沉淀下来的结构仅供参考重点是理解每一层为什么存在。my_fastapi_project/ ├── app/ │ ├── __init__.py │ ├── main.py # 应用入口创建FastAPI实例注册路由 │ ├── core/ │ │ ├── config.py # 配置项读取.env、环境变量 │ │ └── database.py # 数据库引擎和会话管理 │ ├── models/ # SQLAlchemy ORM模型 │ │ └── book.py │ ├── schemas/ # Pydantic模型请求/响应 │ │ └── book.py │ ├── crud/ # 数据库操作层复用性高 │ │ └── book.py │ ├── routers/ # API路由层 │ │ └── book.py │ └── dependencies.py # 公共依赖如get_db ├── .env ├── requirements.txt └── README.md这里我想多说一句设计思路。models、schemas、crud、routers这四层是分离的每层职责单一模型层只管数据库表映射Schema层只管API的输入输出定义CRUD层只管数据操作逻辑路由层只管接口定义和参数绑定。这样做的好处是当你需要新增一张表、一个接口或者改一个校验规则时改动范围可以被精确控制不会牵一发动全身。如果你的项目更大建议进一步按业务域拆分子包而不是把所有模型堆在一个models目录里。比如用户域、订单域、商品域各自建包每个包内包含自己的models、schemas、routers。这是我的核心经验目录结构跟着业务边界走而不是跟着技术组件走。2.3 最小可运行应用先把最小骨架跑起来后面再一步步填充细节。from fastapi import FastAPI app FastAPI(title我的第一个FastAPI应用, version0.1.0) app.get(/) def read_root(): return {message: Hello FastAPI}保存为main.py启动uvicorn main:app --reload --port 8000--reload参数开启热重载改代码后服务自动重启开发期必备。但要注意热重载只建议在开发环境用生产环境开热重载会白白消耗资源还可能因为代码变更触发意外的重启。启动后访问http://127.0.0.1:8000/docs你就能看到自动生成的Swagger文档了。到这里别急着高兴接下来要解决的是实际开发中躲不开的几个核心配置。3. 跨域配置CORS的原理与FastAPI实践3.1 前后端分离为什么会撞上跨域fastapi cors能成为热搜词说明跨域问题卡住了不少人。简单说浏览器的同源策略规定一个页面只能请求同协议、同域名、同端口下的资源。前端开发服务器跑在localhost:3000后端FastAPI跑在localhost:8000端口不一样天然就是跨域。浏览器遇到跨域请求时会先分情况处理简单的GET/POST请求直接发出但带自定义Header、非简单Content-Type的请求会先发一个OPTIONS预检请求问服务器允不允许我这么干服务器点头后浏览器才发真正的请求。预检请求处理不好前端就会看到CORS policy: No Access-Control-Allow-Origin header这类报错。3.2 FastAPI中配置CORSMiddlewareFastAPI提供了现成的中间件来处理跨域。你不需要自己写任何有关Header的逻辑用CORSMiddleware配一下就行。from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins[ http://localhost:3000, http://127.0.0.1:3000, ], allow_credentialsTrue, allow_methods[GET, POST, PUT, DELETE, OPTIONS], allow_headers[*], )参数看着简单但有几个地方非常容易踩坑。先说allow_origins。开发环境图省事可能直接填[*]表示允许所有来源。但如果你同时设置了allow_credentialsTrue那么浏览器会拒绝这次跨域请求因为带Cookie的跨域请求不允许Access-Control-Allow-Origin为星号。所以只要你将来有可能用到Cookie或Session认证allow_origins就必须写成具体的域名列表。3.3 实际项目中的CORS注意事项第一个注意点是allow_credentialsTrue什么时候用。很多项目用JWT把token放在Authorization Header里这种情况下其实allow_credentials可以设False因为JWT不走Cookie。但如果你做的是传统登录态用Cookie存Session ID那allow_credentials必须为True否则浏览器拿不到Set-Cookie响应头登录态根本建立不起来。第二个注意点是本地开发时localhost和127.0.0.1虽然指向同一台机器但浏览器把它俩视为不同源。所以配置里两个地址最好都写上不然前端用localhost访问你在allow_origins里只写了127.0.0.1照样报跨域错。第三个注意点是中间件的添加顺序。理论上app.add_middleware哪一行加都行但如果你同时用了多个自定义中间件要理解中间件是洋葱模型请求按添加顺序从外到内进入响应从内到外返回。CORS中间件通常要放在最外层确保预检请求能第一时间被处理掉。4. 接入SQLAlchemy构建高性能Web服务4.1 SQLAlchemy 2.0风格的模型定义fastapi和sqlalchemy构建高性能web服务这个组合是FastAPI项目的主流方案。FastAPI本身不做ORM数据库操作全靠SQLAlchemy。我用的是SQLAlchemy 2.0风格和旧版写法有区别如果你看网上的教程很多还是1.x老风格注意分辨。2.0风格用DeclarativeBase来定义基类模型的列类型用Mapped和mapped_column来声明。from datetime import datetime from sqlalchemy import String, Float, DateTime, func from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column class Base(DeclarativeBase): pass class Book(Base): __tablename__ books id: Mapped[int] mapped_column(primary_keyTrue, indexTrue) title: Mapped[str] mapped_column(String(200), indexTrue) author: Mapped[str] mapped_column(String(100)) price: Mapped[float] mapped_column(Float) stock: Mapped[int] mapped_column(default0) created_at: Mapped[datetime] mapped_column(DateTime, server_defaultfunc.now())2.0风格最大的好处是类型安全Mapped[str]告诉SQLAlchemy这个字段的类型是strIDE和mypy都能基于这个做静态检查。mapped_column替代了旧的Column写法上更简洁。server_defaultfunc.now()的意思是让数据库在插入时生成当前时间这个比Python侧defaultdatetime.now更可靠因为数据库层的默认值在直接执行SQL时也生效。4.2 数据库连接与会话管理接下来是建立数据库连接。我以MySQL为例因为这是生产环境最常见的配置。别忘了装对应的驱动PyMySQL是纯Python的简单但慢asyncmy是异步驱动性能好和FastAPI的异步模型更搭。from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker, Session from typing import Generator DATABASE_URL mysqlpymysql://root:passwordlocalhost/fastapi_demo?charsetutf8mb4 engine create_engine( DATABASE_URL, pool_size10, max_overflow20, pool_pre_pingTrue, ) SessionLocal sessionmaker(bindengine, autoflushFalse, autocommitFalse) def get_db() - Generator[Session, None, None]: db SessionLocal() try: yield db finally: db.close()这里有一个非常关键的设计get_db这个依赖函数。FastAPI的依赖注入系统会在请求进来时调用get_db当一个请求结束时无论接口逻辑是否抛异常finally里的db.close()都会执行确保数据库连接被释放回连接池。我见过很多新手项目不搞依赖注入每个接口里自己SessionLocal()开一个会话用完忘了close()跑一段时间后数据库连接池就满了应用报Too many connections错误排查起来特别痛苦。所以get_db这个模式不是锦上添花而是必须养成的习惯。pool_size10表示连接池里维持10个连接max_overflow20表示连接不够用的时候最多额外创建20个。pool_pre_pingTrue会在每次取连接时发一个轻量ping包确认连接还活着。这对于跑在云环境、数据库连接可能被中间网络设备回收的场景特别有用能避免Connection has been closed这种偶发报错。4.3 高性能异步方案如果你的项目对并发要求高可以上SQLAlchemy异步版本。异步版要换一套API不能和同步版混用。from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession from typing import AsyncGenerator DATABASE_URL mysqlasyncmy://root:passwordlocalhost/fastapi_demo?charsetutf8mb4 async_engine create_async_engine( DATABASE_URL, pool_size10, max_overflow20, pool_pre_pingTrue, echoFalse, ) AsyncSessionLocal async_sessionmaker( async_engine, class_AsyncSession, expire_on_commitFalse, ) async def get_async_db() - AsyncGenerator[AsyncSession, None]: async with AsyncSessionLocal() as session: yield session使用异步数据库时路由函数必须用async def定义而且所有数据库操作都要加awaitfrom sqlalchemy import select router.get(/books/{book_id}) async def get_book(book_id: int, db: AsyncSession Depends(get_async_db)): result await db.execute(select(Book).where(Book.id book_id)) book result.scalar_one_or_none() if not book: raise HTTPException(status_code404, detailBook not found) return book我在实际项目中遇到过一个问题很多教程里讲result.scalar_one_or_none()取单条结果但如果你没注意查询可能返回多条SQLAlchemy会直接抛MultipleResultsFound异常。在设计中get_book按主键查永远只有零条或一条所以用这个方法是安全的。如果你按某个非唯一字段查要改用scalars().first()或者手动处理多条结果。4.4 分页、过滤与索引设计列表接口必须做分页否则数据量一大一次查全表既慢又浪费内存。最基础的方案是limit/offset分页。router.get(/books/) def list_books( skip: int 0, limit: int 10, db: Session Depends(get_db), ): books db.query(Book).order_by(Book.id).offset(skip).limit(limit).all() return books但你得知道limit/offset的软肋偏移量越大数据库要扫的行越多。offset100000时数据库得先查出前100010行再丢掉前100000行效率非常低。数据量超过几十万之后更推荐用游标分页也叫keyset pagination核心思路是用上次返回的最后一个id作为查询条件。router.get(/books/) def list_books( after_id: int 0, limit: int 10, db: Session Depends(get_db), ): books db.query(Book).where(Book.id after_id).order_by(Book.id).limit(limit).all() return books这种分页方式的性能不随页数增加而下降因为where id ?配合主键索引可以瞬间定位数据位置。当然它也有局限无法跳页只能一页一页往下翻。适合做加载更多这种交互形式不适合做带页号跳转的后台管理系统。索引的设计同样重要。查询频繁用的条件字段比如title、author可以加indexTrue。但索引不是越多越好每个索引都会拖慢写入速度还会占用磁盘空间。我的习惯是先根据实际查询语句分析where和排序字段只给高频查询加索引宁缺毋滥。5. 实战纯REST接口从零到一5.1 需求设计与接口规划这个部分我们做一个完整的图书管理REST接口把前面讲的知识点串起来。REST接口的设计核心是资源用名词复数命名操作语义通过HTTP方法表达状态码准确传达结果。方法路径语义成功状态码失败状态码POST/books/创建图书201 Created422 参数校验失败GET/books/获取图书列表分页200 OK-GET/books/{book_id}获取图书详情200 OK404 不存在PUT/books/{book_id}更新图书200 OK404/422DELETE/books/{book_id}删除图书204 No Content404纯REST接口有个容易忽略的点创建成功应该返回201删除成功应该返回204空响应体而不是全部返回200。很多后端同学习惯统一返回200然后自己在body里塞一个{code: 1, msg: success}这在REST语义下是不推荐的。HTTP状态码本身就是表达语义的工具搞一套自定义code来包装等于把工具废了又自己造轮子。5.2 模型与Schema定义模型层已经在4.1节定义好了现在定义Pydantic的Schema。这里我遵循一个最佳实践输入的Schema和输出的Schema分开定义。from pydantic import BaseModel, ConfigDict, Field from datetime import datetime class BookCreate(BaseModel): title: str Field(..., min_length1, max_length200, description书名) author: str Field(..., min_length1, max_length100, description作者) price: float Field(..., gt0, description价格) stock: int Field(0, ge0, description库存) class BookUpdate(BaseModel): title: str | None Field(None, min_length1, max_length200) author: str | None Field(None, min_length1, max_length100) price: float | None Field(None, gt0) stock: int | None Field(None, ge0) class BookRead(BaseModel): model_config ConfigDict(from_attributesTrue) id: int title: str author: str price: float stock: int created_at: datetimeBookCreate里Field(..., min_length1)表示必填且长度至少1前端漏传或传空字符串都会得到422。BookUpdate里所有字段都是可选这是为了支持部分更新。最关键的BookRead里的ConfigDict(from_attributesTrue)它告诉Pydantic可以从SQLAlchemy模型实例直接转换。没有这一行把ORM对象当作响应返回时会报AttributeError这是Pydantic v2的写法v1的老代码写的是class Config: orm_mode True网上旧教程会让你踩这个坑。输入输出分离的理由很实际客户端的BookCreate不携带id和created_at因为这是服务端生成的字段用户传了也不应该被接受而BookRead则展示完整资源信息。如果共用同一个Schema很容易出现客户端把不可控字段传上来或者响应里多出不该出现的内部字段。5.3 路由与CRUD接口实现先把路由器和依赖注入接起来。from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session from app.schemas.book import BookCreate, BookUpdate, BookRead from app.models.book import Book from app.dependencies import get_db router APIRouter(prefix/books, tags[books])创建图书的接口核心是三步接收Schema、写入数据库、返回带id和created_at的完整对象。router.post(/, response_modelBookRead, status_codestatus.HTTP_201_CREATED) def create_book(payload: BookCreate, db: Session Depends(get_db)): book Book(**payload.model_dump()) db.add(book) db.commit() db.refresh(book) return book这里有几个关键点。payload.model_dump()是Pydantic v2里官方推荐的方法v1里叫dict()返回一个普通字典然后通过Book(**dict)拆包成ORM实例。db.refresh(book)的作用是从数据库重新拉取这行数据因为id和created_at是数据库生成的不refresh的话返回给前端时这两个字段是空的。提交之后session里的对象状态未过期但自增主键和数据库默认值不会被自动回填所以refresh这步必须做。获取列表接口支持分页和标题模糊搜索router.get(/, response_modellist[BookRead]) def list_books( skip: int 0, limit: int 10, title: str | None None, db: Session Depends(get_db), ): query db.query(Book) if title: query query.filter(Book.title.contains(title)) books query.order_by(Book.id).offset(skip).limit(limit).all() return booksBook.title.contains(title)会生成WHERE title LIKE %xxx%的SQL适合小规模模糊搜索。当数据量大了之后%xxx%这种写法因为最前面有通配符数据库没法走索引查询会退化成全表扫描。这时候要么用全文索引要么引入专门的搜索引擎先有个预期后面才不会措手不及。更新接口用PUT这里有个技巧router.put(/{book_id}, response_modelBookRead) def update_book( book_id: int, payload: BookUpdate, db: Session Depends(get_db), ): book db.get(Book, book_id) if not book: raise HTTPException(status_codestatus.HTTP_404_NOT_FOUND, detailBook not found) update_data payload.model_dump(exclude_unsetTrue) for field, value in update_data.items(): setattr(book, field, value) db.commit() db.refresh(book) return bookexclude_unsetTrue这个参数非常实用。它会让Pydantic只返回客户端显式传过的字段没传的字段值不会被拿出来覆盖数据库里的旧值。这样就能实现客户端传哪个字段就更新哪个字段不传的保持原样。如果漏了这个参数BookUpdate里没传的字段就会以None覆盖原有数据造成莫名其妙的数据丢失。删除接口返回204No Content表示删除成功但没有响应体。router.delete(/{book_id}, status_codestatus.HTTP_204_NO_CONTENT) def delete_book(book_id: int, db: Session Depends(get_db)): book db.get(Book, book_id) if not book: raise HTTPException(status_codestatus.HTTP_404_NOT_FOUND, detailBook not found) db.delete(book) db.commit() return None注意204状态码下FastAPI要求响应体为空所以return None是必须的如果返回一个dict可能会收到Response body is not allowed for 204的警告或报错。另外删除操作会直接物理删除行记录如果业务希望保留历史数据建议给表加一个is_deleted字段查询时统一过滤这种方式叫软删除很多正规项目都这么做。5.4 注册路由与统一响应格式的讨论最后在main.py里注册路由。from fastapi import FastAPI from app.routers import book app FastAPI(title图书管理系统API, version1.0.0) app.include_router(book.router)关于统一响应格式我多说两句。网上很多教程教你把所有接口包一层{code: 0, message: success, data: ...}这种设计在企业内很常见但纯REST的观点是状态码就该用HTTP的成功和失败的信息就该用HTTP状态码和错误体表达没必要再套一层。我的倾向是对外公开的API尽量保持纯REST响应体直接就是资源本身错误通过HTTP状态码和detail字段表达如果是对内服务且历史约定就是包一层那就保持团队一致不要混着来。这里没有绝对的对错但一定要统一混用会让前端对接的人疯掉。6. 常见问题与排查技巧实录6.1 问题速查表这一节我把实战中遇到的高频问题整理成一张速查表方便大家按图索骥。现象原因解决办法启动时报AttributeError: BaseQuery object has no attribute filter_bySQLAlchemy版本混用检查是否混用了2.0和1.x写法统一用2.0风格Pydantic报orm_mode相关错误教程基于Pydantic v1v2改为ConfigDict(from_attributesTrue)接口返回422而不是自定义的400Pydantic校验不通过这是正常行为422表示语义正确但校验失败前端跨域请求在OPTIONS阶段就挂了CORS配置不完整检查allow_origins是否包含实际来源且含OPTIONS方法偶发MySQL Connection is not available连接被网络设备回收pool_pre_pingTrue页面请求一次后卡死异步路由里用了同步数据库调用改用异步引擎或用def定义路由SQLite报database is locked并发写同一sqlite文件开发环境换成PostgreSQL或MySQL修改代码不生效--reload没开或改了没触发确认启动命令带--reload6.2 几个印象深刻的踩坑经历第一个坑是关于缓存失效的。每个请求挂了数据库连接池之后某个凌晨收到告警数据库连接数直线上升最后把连接池打爆导致服务不可用。查下来发现是某些慢查询把连接占用时间拉长了新请求不断申请新连接max_overflow20很快就用满了。这个问题的根源不是连接池配置而是慢查询。所以连接池参数只是兜底真正要做的是把响应时间控制在合理范围同时监控慢查询日志。第二个坑是异步和同步的混用。有一次在async def路由里直接调用同步SQLAlchemy的db.query方法查询数据接口正常运行但一旦并发高起来整个事件循环被数据库阻塞其他接口也跟着卡。排查半天才意识到问题所在同步数据库操作是阻塞式的在异步事件循环里执行相当于把所有请求串行排队了。解决方案是要么全部走异步引擎要么把这个路由改成普通的defFastAPI会自动丢到线程池执行不会阻塞主循环。这个坑不止我一个人踩热搜里的fastapi和sqlalchemy构建高性能web服务大概率也有不少人在找这个答案。第三个坑是关于字段默认值的。项目开发阶段用SQLitecreated_at字段用Python侧defaultdatetime.now一切正常。迁移到MySQL后发现直接通过SQL插入的数据没有创建时间报错才发现Python侧默认值在数据库层根本不存在。后来统一改成server_defaultfunc.now()让数据库自己生成时间才彻底解决。这个教训告诉我写ORM模型时凡是数据库能自己搞定的默认值就不要依赖应用层。6.3 生产环境部署要点部署这块简单说说。直接跑uvicorn app.main:app是开发模式生产环境需要多进程和进程管理。推荐用gunicorn作为进程管理器让uvicorn充当ASGI workergunicorn app.main:app -k uvicorn.workers.UvicornWorker -w 4 --bind 0.0.0.0:8000worker数量一般建议CPU核心数*21比如4核机器就开9个worker这个公式是Gunicorn官方文档推荐的经验值。如果觉得手工维护太麻烦Dockerfile方案也很成熟FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8000 CMD [gunicorn, app.main:app, -k, uvicorn.workers.UvicornWorker, -w, 4, --bind, 0.0.0.0:8000]--no-cache-dir能让镜像小不少python:3.11-slim比python:3.11体积小很多部署时要刻意关注镜像体积特别是服务数量多的时候。还有一个容易被忽略的点Docker里跑的进程默认是root权限存在安全风险正规做法是单独建一个非root用户运行应用。这一点在团队协作时可能不那么被重视但作为个人项目上线时一定要养成习惯。6.4 性能优化的几条实测建议性能优化是fastapi和sqlalchemy构建高性能web服务这个热搜词绕不开的话题。我的实测经验集中在四条第一尽量用async def定义路由函数。第二数据库查询尽量精确只select需要的列避免select *。第三热点数据上Redis缓存特别是那种被频繁读取但很少变化的数据数据库压力能降两个数量级。第四接口里的逻辑如果能合并成一条SQL就不要拆成多次查询往返。缓存这块举个例子。图书列表接口如果每个用户进来都查一次数据库数据库压力会很大。用Redis缓存列表数据设置60秒过期就能显著降低数据库负载。实现也简单import json import redis r redis.Redis(hostlocalhost, port6379, db0) router.get(/) def list_books(db: Session Depends(get_db)): cache_key books:list cached r.get(cache_key) if cached: return json.loads(cached) books db.query(Book).order_by(Book.id).limit(10).all() r.setex(cache_key, 60, json.dumps([b.__dict__ for b in books], defaultstr)) return books注意一个细节缓存的数据如果是ORM对象序列化时要处理datetime字段json.dumps默认不认识datetime对象要加defaultstr或者先转换为字符串。这里我展示的是最简写法生产环境建议引入专门的序列化方案避免反复踩坑。在最后再分享一个实践技巧当你使用依赖注入的get_db模式后写接口的单元测试会变得很轻松。测试时只需要用依赖覆盖机制把get_db替换成一个测试用的会话工厂就能对每个接口做隔离测试不需要真的连生产数据库。这个操作在FastAPI里只需要一行app.dependency_overrides[get_db] get_test_db这个机制是我个人最喜欢的FastAPI特性之一。它让测试代码极度干净不用mocking网络请求、不用起真实服务直接调用路由函数或者用TestClient发请求数据都落在测试专用的数据库里跑完就清理。前面我们把项目结构拆得那么清晰依赖注入又提供了接口隔离的基础写起测试来顺手很多。FastAPI这套东西上手之后写接口的效率确实比传统框架高出一截。从一开始被类型注解的写法搞得有点不适应到后来习惯了先定义Schema再写路由整个开发流顺畅了很多。这篇整理了项目从零到上线的完整链路希望对你有所帮助。