FastAPI 与异步服务开发实战

FastAPI 自带异步支持,但写好异步服务不只是把 def 改成 async def。从异步路由、后台任务到 WebSocket 与异步数据库驱动,本文讲清 FastAPI 异步服务开发的完整模式与常见坑。

FastAPI 异步服务配图:蓝色渐变背景上层叠服务器方块与环形事件流,扁平科技插画
FastAPI 异步:事件循环驱动的高并发 Web 服务

一、为什么 FastAPI 天生异步

FastAPI 底层基于 Starlette 和 Uvicorn,天然运行在 asyncio 事件循环上。这意味着你写的路由函数如果是 async def,它就在事件循环里直接执行;如果是普通 def,FastAPI 会自动把它放到线程池里跑。理解这个区别是写好异步 FastAPI 的第一步。

from fastapi import FastAPI
import asyncio

app = FastAPI()

# 异步路由:直接在事件循环执行
@app.get("/async")
async def async_endpoint():
    await asyncio.sleep(1)  # 不阻塞事件循环
    return {"mode": "async"}

# 同步路由:FastAPI 自动放到线程池
@app.get("/sync")
def sync_endpoint():
    import time
    time.sleep(1)  # 阻塞线程但不阻塞事件循环
    return {"mode": "sync"}
关键原则:路由函数里有 await 就用 async def;全是同步代码(如 CPU 密集计算)就用普通 def,FastAPI 会帮你调度。不要在 async 路由里调用阻塞代码,那会卡住整个事件循环。

二、异步数据库操作

用同步驱动(如 psycopg2)查数据库会阻塞事件循环。异步服务要用异步驱动(如 asyncpg、SQLAlchemy 2.0 async)。

from fastapi import FastAPI, Depends
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from sqlalchemy import text

DATABASE_URL = "postgresql+asyncpg://user:pass@localhost/db"
engine = create_async_engine(DATABASE_URL)
async_session = async_sessionmaker(engine, expire_on_commit=False)

app = FastAPI()

async def get_db():
    async with async_session() as session:
        yield session

@app.get("/users/{user_id}")
async def get_user(user_id: int, db: AsyncSession = Depends(get_db)):
    result = await db.execute(text("SELECT * FROM users WHERE id = :id"), {"id": user_id})
    user = result.first()
    return {"user": user}

三、后台任务

有些操作不需要等它完成再返回响应,比如发邮件、写日志。FastAPI 的 BackgroundTasks 可以在响应返回后异步执行。

from fastapi import FastAPI, BackgroundTasks

app = FastAPI()

def send_email(email: str, message: str):
    # 模拟发邮件(实际用 smtplib 或 aiosmtplib)
    print(f"给 {email} 发送: {message}")

@app.post("/register")
async def register(email: str, background_tasks: BackgroundTasks):
    background_tasks.add_task(send_email, email, "欢迎注册!")
    return {"status": "ok", "message": "注册成功,确认邮件已发送"}

客户端立即收到响应,发邮件在后台慢慢跑。注意 BackgroundTasks 是同进程内的轻量任务,如果任务很重或需要重试,应该用 Celery 或 RQ。

四、WebSocket 实时通信

FastAPI 原生支持 WebSocket,适合聊天、实时通知、数据流推送。

from fastapi import FastAPI, WebSocket, WebSocketDisconnect

app = FastAPI()

class ConnectionManager:
    def __init__(self):
        self.active = []

    async def connect(self, ws: WebSocket):
        await ws.accept()
        self.active.append(ws)

    def disconnect(self, ws: WebSocket):
        self.active.remove(ws)

    async def broadcast(self, message: str):
        for ws in self.active:
            await ws.send_text(message)

manager = ConnectionManager()

@app.websocket("/ws/chat")
async def websocket_chat(ws: WebSocket):
    await manager.connect(ws)
    try:
        while True:
            data = await ws.receive_text()
            await manager.broadcast(f"收到: {data}")
    except WebSocketDisconnect:
        manager.disconnect(ws)

五、并发请求处理

在一个路由里需要同时调多个外部 API 时,用 asyncio.gather 并发执行,比串行快很多。

import httpx
import asyncio
from fastapi import FastAPI

app = FastAPI()

@app.get("/aggregate")
async def aggregate():
    async with httpx.AsyncClient() as client:
        # 并发请求三个 API
        results = await asyncio.gather(
            client.get("https://api.example.com/users"),
            client.get("https://api.example.com/products"),
            client.get("https://api.example.com/orders"),
        )
    return {
        "users": results[0].json(),
        "products": results[1].json(),
        "orders": results[2].json(),
    }

六、常见坑与最佳实践

坑一:async 路由里调阻塞代码。在 async def 里用 requests.get() 会阻塞整个事件循环。改用 httpx.AsyncClient。

坑二:忽略连接池。高并发下不要每次请求都新建数据库连接,用连接池(SQLAlchemy async engine 自带连接池)。

坑三:测试用同步客户端。测试异步路由要用 httpx.AsyncClient 或 FastAPI 的 TestClient(它内部自动处理异步)。

七、总结

FastAPI 的异步能力是它相比 Flask 的最大优势。正确使用方式:IO 密集操作用异步(HTTP 请求、数据库查询、WebSocket),CPU 密集操作用同步路由交给线程池。记住一个判断标准:你的代码里有没有等待外部资源?有就用 async,没有就用 def。