第16课:FastAPI|Python异步编程基础|异步数据库与异步接口开发实战

在这里插入图片描述

文章目录


1. 课前导读

本节课学习目标

在前15课中,我们已经能够开发功能完善的RESTful API,并集成了MySQL数据库实现了CRUD操作。你可能已经注意到,FastAPI的路由函数既可以定义为def(同步)也可以定义为async def(异步)。那么,这两者有什么区别?为什么FastAPI能够比传统的Flask框架处理更高的并发?答案就在于异步编程

本节课将从理论到实践,系统讲解Python异步编程的核心概念,以及如何在FastAPI中充分利用异步特性构建高性能接口。学完本节课,你将:

  • 理解异步编程原理:事件循环、协程、async/await的底层逻辑
  • 掌握asyncio基础:创建任务、并发执行、同步原语
  • 优化异步数据库操作:确保数据库驱动为异步,避免阻塞事件循环
  • 实现并发请求聚合:使用asyncio.gather同时调用多个外部API或数据库查询,大幅降低接口响应时间
  • 区分同步与异步路由:知道何时使用async def、何时使用普通def,避免误区
  • 解决异步中的常见陷阱:混用同步阻塞库、忘记await、事件循环阻塞等

前置知识

  • 已完成第15课,掌握SQLAlchemy异步ORM的基本使用
  • 了解Python函数定义,对async/await有基础认知(即使不熟悉也没关系,本课会详细讲解)
  • 了解HTTP协议和并发的基本概念

学完能掌握什么

学完本节课后,你将具备以下能力:

  1. 编写高效的异步接口:在IO密集型场景下,让API并发能力提升数倍
  2. 使用asyncio.gather并发执行多个数据库查询:将串行300ms的总时间降低到并行100ms
  3. 集成异步HTTP客户端httpx:并发调用多个第三方API,聚合结果
  4. 正确使用BackgroundTasksasyncio.create_task:处理后台任务,不阻塞响应
  5. 识别和解决异步代码中的阻塞点:将同步库放到线程池中执行,保持事件循环流畅
  6. 为生产环境性能调优打下基础:理解异步与多进程的区别,合理部署

适用人群

  • 对Python异步一知半解,想要彻底搞懂的开发者
  • 已经用FastAPI写了接口,但不确定自己的代码是否真正异步的工程师
  • 接口性能不佳,希望通过并发优化响应时间的后端开发
  • 准备构建高并发、高吞吐量API的架构师

2. 核心理论讲解

2.1 同步 vs 异步:从餐厅服务讲起

同步模型(如Flask、Django传统模式):
服务员(线程)接到订单后,必须等待厨师做完菜(IO操作),期间不能服务其他客人。为了服务多位客人,餐厅必须雇佣很多服务员(多线程/多进程),但每个服务员占用大量资源。

异步模型(如FastAPI + 异步驱动):
服务员(协程)接到订单后,只需将订单交给厨师,然后立即去服务下一位客人;当厨师做好菜时,服务员再回来取菜送给客人。一个服务员可以同时处理成百上千个订单,资源占用极少。

核心区别

  • 同步:IO操作时线程阻塞,等待完成。
  • 异步:IO操作时协程主动让出控制权,事件循环调度其他协程执行,IO完成后恢复。

2.2 事件循环与协程的底层逻辑

事件循环(Event Loop) 是异步编程的引擎。它不断循环检查任务队列,执行可运行的协程,处理IO事件。可以理解为“总调度室”。

协程(Coroutine) 是一种特殊的函数,可以在执行过程中暂停(await),让出控制权给事件循环,等待某个IO操作完成后恢复执行。

async defawait 的作用

  • async def:定义一个协程函数,调用时返回协程对象,不会立即执行。
  • await:暂停当前协程,等待另一个协程或Future完成,期间事件循环可以执行其他任务。

简单示例

import asyncio

async def say_after(delay, msg):
    await asyncio.sleep(delay)  # 模拟IO操作
    print(msg)

async def main():
    # 并发执行两个任务
    task1 = asyncio.create_task(say_after(1, "Hello"))
    task2 = asyncio.create_task(say_after(2, "World"))
    await task1
    await task2

asyncio.run(main())

2.3 FastAPI如何处理同步与异步路由

FastAPI支持两种定义方式:

@app.get("/sync")
def sync_route():
    return {"message": "同步路由"}

@app.get("/async")
async def async_route():
    return {"message": "异步路由"}

工作原理

  • 如果路由是async def,FastAPI会直接将其作为协程在事件循环中调用。
  • 如果路由是def,FastAPI会使用run_in_executor将其放到线程池中执行,避免阻塞事件循环。

性能影响

  • IO密集型操作(数据库查询、HTTP请求、文件读写):应使用async def+异步库,真正实现高并发。
  • CPU密集型操作(计算、加密、图像处理):应使用def,交由线程池处理,避免占用事件循环。或者使用asyncio.to_thread

重要:如果在async def路由中调用了同步阻塞库(如requests.get()),整个事件循环会被阻塞,所有请求串行执行,性能急剧下降。必须使用异步版本(如httpx.AsyncClient)。

2.4 异步数据库驱动的必要性

在第14课中,我们使用了aiomysql作为MySQL的异步驱动,配合SQLAlchemy异步引擎。为什么不能用pymysql

  • pymysql是同步驱动,执行查询时会阻塞当前线程。如果在async def路由中使用,会阻塞事件循环。
  • aiomysql底层使用asyncio进行非阻塞IO,查询期间事件循环可以处理其他请求。

验证阻塞:编写一个使用同步驱动的接口,然后使用time.sleep模拟,你会发现并发请求全部串行处理。而异步驱动下,await asyncio.sleep不会阻塞。

2.5 并发执行:asyncio.gather vs 串行

假设我们需要在同一个接口中查询用户信息、订单列表、商品推荐三个独立数据源。如果串行执行:

用户查询: 50ms → 订单查询: 80ms → 推荐查询: 100ms → 总耗时 230ms

如果并发执行(同时发起三个查询),总耗时仅为最慢的那个(100ms),性能提升130%。

使用asyncio.gather

results = await asyncio.gather(
    fetch_user(),
    fetch_orders(),
    fetch_recommendations()
)

2.6 与Flask/Django的异步对比

框架原生异步支持异步ORM/驱动并发能力学习曲线
FastAPI完全异步(ASGI)SQLAlchemy 2.0异步、asyncpg、aiomysql极高中等
Flask同步(WSGI),可通过Quart变异步需额外配置低(多线程)
Django 3.1+部分异步视图,但ORM同步需使用asyncio封装或第三方有限中高

FastAPI基于Starlette(ASGI),天然支持WebSocket、HTTP/2,异步生态成熟。


3. 环境搭建 & 实操准备

3.1 复用项目环境

继续使用第15课的项目(已完成数据库集成)。确保虚拟环境已激活。

cd ~/fastapi_course/lesson03_first_project
source venv/bin/activate

3.2 安装异步HTTP客户端

为了演示并发调用外部API,安装httpx(支持异步):

pip install httpx

3.3 确认数据库驱动为异步

检查.env中的DATABASE_URL是否使用aiomysql

DATABASE_URL=mysql+aiomysql://root:password@localhost:3306/fastapi_course?charset=utf8mb4

3.4 新增代码文件

创建app/api/v1/async_demo.py,并在main.py中注册。

touch app/api/v1/async_demo.py

main.py中注册:

from app.api.v1 import async_demo
app.include_router(async_demo.router, prefix="/api/v1/async", tags=["异步编程演示"])

为了模拟真实场景,我们也会创建一个简单的第三方API模拟服务(使用FastAPI的另一个实例或使用公共API)。本课使用httpbin.org作为演示。


4. 手把手代码实战

4.1 asyncio基础:协程、任务、事件循环

首先,创建一个演示协程基础的路由,不涉及HTTP,仅帮助理解概念。

# app/api/v1/async_demo.py
import asyncio
from fastapi import APIRouter

router = APIRouter()

@router.get("/basics")
async def async_basics():
    """
    演示异步基础概念
    """
    async def hello_world():
        await asyncio.sleep(0.1)
        return "Hello"
    
    async def foo():
        await asyncio.sleep(0.2)
        return "World"
    
    # 串行执行
    start = asyncio.get_event_loop().time()
    result1 = await hello_world()
    result2 = await foo()
    serial_time = asyncio.get_event_loop().time() - start
    
    # 并发执行
    start = asyncio.get_event_loop().time()
    results = await asyncio.gather(hello_world(), foo())
    parallel_time = asyncio.get_event_loop().time() - start
    
    return {
        "serial_result": f"{result1} {result2}",
        "serial_time": round(serial_time * 1000, 2),
        "parallel_result": " ".join(results),
        "parallel_time": round(parallel_time * 1000, 2),
        "conclusion": f"并发节省了 {round((serial_time - parallel_time) * 1000, 2)}ms"
    }

访问/api/v1/async/basics,可以看到并发比串行快约100ms(因为两个sleep并行)。

代码说明

  • asyncio.sleep是一个异步等待操作,模拟IO等待。
  • asyncio.gather并发执行多个协程,等待所有完成。
  • 使用事件循环的时间戳测量耗时。

4.2 同步阻塞 vs 异步非阻塞路由对比

为了直观感受,我们创建两个对比接口:一个使用同步阻塞(time.sleep),一个使用异步非阻塞(asyncio.sleep)。

import time

@router.get("/blocking")
def blocking_route():
    """同步阻塞路由 - 会阻塞整个服务器"""
    time.sleep(3)  # 模拟阻塞操作
    return {"message": "完成"}

@router.get("/non-blocking")
async def non_blocking_route():
    """异步非阻塞路由 - 不会阻塞"""
    await asyncio.sleep(3)  # 异步等待,让出控制权
    return {"message": "完成"}

测试方法:使用浏览器或curl同时发送多个请求到/blocking,你会注意到第二个请求需要等待第一个完成后才能开始(串行)。而/non-blocking多个请求可以同时处理。

注意:在生产环境中,绝不要在async def路由中使用time.sleep,必须用asyncio.sleep

4.3 异步数据库查询(使用现有User模型)

我们使用第15课的用户表,编写一个接口并发查询多个用户。

from sqlalchemy import select
from app.database import get_db
from app.models.user import User
from app.schemas.user import UserResponse

@router.get("/users/concurrent")
async def get_users_concurrent(
    db: AsyncSession = Depends(get_db),
    ids: str = Query("1,2,3", description="用户ID列表,逗号分隔")
):
    """
    并发查询多个用户,使用 asyncio.gather
    """
    id_list = [int(i.strip()) for i in ids.split(",")]
    
    async def fetch_one(user_id: int):
        stmt = select(User).where(User.id == user_id)
        result = await db.execute(stmt)
        user = result.scalar_one_or_none()
        return user
    
    # 并发执行所有查询
    users = await asyncio.gather(*[fetch_one(uid) for uid in id_list])
    
    # 过滤掉不存在的用户
    valid_users = [UserResponse.model_validate(u) for u in users if u]
    return {"total_requested": len(id_list), "found": len(valid_users), "users": valid_users}

性能分析:如果串行查询3个用户,每个耗时50ms,总耗时150ms;并发查询总耗时仍为50ms(假设数据库连接池足够)。

注意:这里每个查询都使用了同一个db会话,SQLAlchemy异步会话是线程安全的,但需要注意的是,asyncio.gather中的协程都共享同一个会话,这通常没问题,因为会话本身支持并发查询(不同的statement)。但如果涉及到事务隔离,需要更谨慎。

4.4 并发调用多个外部API(使用httpx)

真实业务中,经常需要聚合多个第三方服务的数据。使用httpx.AsyncClient并发请求。

import httpx

@router.get("/aggregate")
async def aggregate_apis():
    """
    并发调用多个外部API,聚合结果
    """
    async def fetch_ip():
        async with httpx.AsyncClient() as client:
            resp = await client.get("https://httpbin.org/ip")
            return resp.json()
    
    async def fetch_user_agent():
        async with httpx.AsyncClient() as client:
            resp = await client.get("https://httpbin.org/user-agent")
            return resp.json()
    
    async def fetch_headers():
        async with httpx.AsyncClient() as client:
            resp = await client.get("https://httpbin.org/headers")
            return resp.json()
    
    # 并发执行
    results = await asyncio.gather(fetch_ip(), fetch_user_agent(), fetch_headers())
    
    return {
        "ip": results[0],
        "user_agent": results[1],
        "headers": results[2]
    }

测试:访问该接口,总耗时大约等于最慢的那个API(通常几百毫秒),而不是三者之和。

最佳实践:复用httpx.AsyncClient实例,避免每个请求都创建新客户端。可以在应用启动时创建一个全局客户端,使用依赖注入传递。

4.5 异步接口中的CPU密集型任务处理

如果路由中有计算密集型任务(如加密、图片处理),不应在async def中直接执行,否则会阻塞事件循环。解决方案是使用asyncio.to_thread将其放到线程池中执行。

import hashlib

def cpu_intensive_task(data: str, rounds: int = 100000):
    """模拟CPU密集型计算"""
    for _ in range(rounds):
        hashlib.sha256(data.encode()).hexdigest()
    return f"Processed {data}"

@router.get("/cpu-bound")
async def cpu_bound_route(data: str = "test"):
    """
    使用 asyncio.to_thread 将CPU密集任务放到线程池执行,避免阻塞事件循环
    """
    # 在单独线程中执行同步函数
    result = await asyncio.to_thread(cpu_intensive_task, data, 50000)
    return {"result": result}

注意asyncio.to_thread是Python 3.9+引入,对于低版本可以使用loop.run_in_executor

4.6 后台任务:BackgroundTasks vs asyncio.create_task

FastAPI提供了BackgroundTasks,用于在响应返回后执行轻量级任务。它使用线程池(同步)执行,适合短小任务。对于需要异步支持的任务,可以使用asyncio.create_task

from fastapi import BackgroundTasks

async def async_background_task(message: str):
    await asyncio.sleep(2)
    print(f"后台任务完成: {message}")

@router.post("/bg-task")
async def bg_task_endpoint(background_tasks: BackgroundTasks, msg: str = "hello"):
    # FastAPI的BackgroundTasks会在线程池中执行同步函数,不能直接接受async函数
    # 如果需要异步后台任务,需手动创建task
    asyncio.create_task(async_background_task(msg))
    return {"message": "请求已接收,后台任务执行中"}

警告asyncio.create_task创建的任务如果未等待,在FastAPI应用中可能随主事件循环关闭而消失。对于可靠的后台任务,应使用Celery等任务队列。

4.7 异步上下文管理器与资源管理

异步上下文管理器(async with)用于管理需要异步初始化和清理的资源,如数据库连接、HTTP客户端。

class AsyncResource:
    async def __aenter__(self):
        print("获取资源")
        return self
    async def __aexit__(self, exc_type, exc_val, exc_tb):
        print("释放资源")
        await asyncio.sleep(0.1)

@router.get("/context")
async def async_context_demo():
    async with AsyncResource() as res:
        # 使用资源
        await asyncio.sleep(0.5)
    return {"message": "ok"}

在FastAPI中,依赖注入的get_db使用了async with来管理会话生命周期,这就是异步上下文管理器的典型应用。

4.8 使用anyioasyncio的兼容层

FastAPI基于anyio,一个比asyncio更高级的异步库,支持多种后端。但通常使用asyncio即可。

4.9 实战:构建高性能仪表盘接口

假设我们需要实现一个仪表盘,同时获取用户统计、订单统计、商品统计。这三个统计来自不同的数据库查询。使用asyncio.gather并发执行。

@router.get("/dashboard")
async def dashboard(db: AsyncSession = Depends(get_db)):
    async def user_count():
        result = await db.execute(select(func.count()).select_from(User))
        return result.scalar_one()
    
    # 假设存在订单模型和商品模型
    # async def order_count(): ...
    # async def product_count(): ...
    
    # 并发执行
    counts = await asyncio.gather(user_count())  # 实际应包含多个
    
    return {
        "user_count": counts[0],
        # "order_count": counts[1],
        # "product_count": counts[2]
    }

由于我们没有定义订单和商品模型,此处仅演示思路。

4.10 避免常见异步陷阱

陷阱1:在async def中调用同步阻塞库
@router.get("/bad")
async def bad_route():
    import requests
    resp = requests.get("https://httpbin.org/delay/2")  # 阻塞事件循环!
    return resp.json()

正确做法:使用httpx.AsyncClient

陷阱2:忘记await
@router.get("/missing-await")
async def missing_await():
    result = asyncio.sleep(1)  # 返回协程对象,但未await
    return {"result": result}  # 协程对象无法序列化

正确做法await asyncio.sleep(1)

陷阱3:在协程中混合使用time.sleep
@router.get("/bad-sleep")
async def bad_sleep():
    time.sleep(2)  # 阻塞整个事件循环
    return {"ok": True}

正确做法await asyncio.sleep(2)

陷阱4:在异步环境中使用asyncio.run()

在已有的事件循环中再次调用asyncio.run()会抛出异常。使用awaitasyncio.create_task

陷阱5:数据库查询未使用异步驱动

即使路由是async def,如果使用的数据库驱动是同步的(如psycopg2pymysql),查询操作依然会阻塞。务必使用异步驱动。

4.11 性能测试:压测对比

我们可以编写一个简单的性能测试脚本(使用asynciolocust)来对比同步阻塞接口和异步非阻塞接口的吞吐量。这里给出思路:

# test_perf.py
import asyncio
import httpx

async def send_request(url):
    async with httpx.AsyncClient() as client:
        resp = await client.get(url)
        return resp.status_code

async def main():
    urls = ["http://localhost:8000/api/v1/async/non-blocking"] * 50
    tasks = [send_request(url) for url in urls]
    start = asyncio.get_event_loop().time()
    await asyncio.gather(*tasks)
    end = asyncio.get_event_loop().time()
    print(f"50个请求完成耗时: {end-start:.2f}秒")

asyncio.run(main())

对比/blocking接口,可以看到显著差异。


5. 重点知识点总结

核心语法速查表

场景代码
定义协程async def my_coro(): ...
等待协程await my_coro()
并发执行多个任务results = await asyncio.gather(coro1(), coro2())
创建任务(不等待)task = asyncio.create_task(coro())
同步函数放到线程池await asyncio.to_thread(sync_func, arg)
异步sleepawait asyncio.sleep(seconds)
异步HTTP请求async with httpx.AsyncClient() as client: resp = await client.get(url)
异步数据库查询result = await db.execute(select(...))
后台任务(异步)asyncio.create_task(background_coro())

异步 vs 同步路由选择指南

路由内操作推荐方式原因
仅调用异步库(数据库、HTTP)async def非阻塞,高并发
CPU密集型计算defasync def + to_thread避免阻塞事件循环
混合:少量异步+少量同步async def + 同步部分放到to_thread保持事件循环流畅
几乎全是同步库,无IO等待def简单,FastAPI会自动线程池

易错点与避坑指南

  1. ❌ 在async def路由中使用requests
    解决方案:使用httpx.AsyncClient

  2. ❌ 忘记await导致协程未执行
    检查所有异步调用前是否有await

  3. ❌ 在协程中使用time.sleep
    必须使用await asyncio.sleep()

  4. ❌ 混用同步数据库驱动
    确保SQLAlchemy引擎使用异步驱动(aiomysqlasyncpg)。

  5. ❌ 使用asyncio.run()在已有事件循环中
    使用awaitasyncio.create_task

  6. ❌ 认为async def会自动使同步代码异步
    只有使用了异步IO库才能真正非阻塞。

最佳实践

  1. IO密集型接口统一使用async def,并确保所有依赖库都是异步版本。
  2. 对于CPU密集型任务,使用asyncio.to_thread或单独部署为微服务
  3. 复用HTTP客户端实例:在应用启动时创建httpx.AsyncClient,通过依赖注入共享。
  4. 使用asyncio.gather而不是串行await,提升并发效率。
  5. 设置合理的超时:对异步操作使用asyncio.wait_for(coro, timeout),避免无限等待。
  6. 在生产环境中监控事件循环延迟:如果事件循环被阻塞,会导致所有请求延迟增加。

6. 课后作业 & 思考题

实操练习题(必做)

  1. 基础练习:改造第15课的用户列表接口,使其支持异步并发统计

    • 在原有分页基础上,增加一个统计字段total_orders(假设存在订单表),使用asyncio.gather并发查询用户列表和总订单数。
  2. 进阶练习:实现一个接口,并发调用三个公共API(如天气、新闻、汇率),聚合返回

    • 使用httpx.AsyncClient
    • 处理其中一个API超时的情况(使用asyncio.wait
  3. 挑战练习:实现一个异步WebSocket广播服务

    • 创建WebSocket端点,客户端连接后,服务端每秒推送当前时间
    • 使用asyncio.create_task管理每个连接的发送任务

理论思考题

  1. 为什么在async def路由中调用同步阻塞库会严重降低并发能力? 请从事件循环角度解释。
  2. asyncio.gatherasyncio.wait的区别是什么? 分别适用于什么场景?
  3. FastAPI的BackgroundTasksasyncio.create_task有什么不同? 何时使用哪个?
  4. 如何检测异步代码中的阻塞点? 有哪些工具或方法?

拓展学习方向

  • 阅读asyncio官方文档:深入了解事件循环策略、传输协议等高级特性
  • 学习anyiotrio:更安全的异步库,FastAPI底层使用anyio
  • 使用asyncio.run_in_executorconcurrent.futures.ProcessPoolExecutor:处理多核CPU密集型任务
  • 异步设计模式:生产者-消费者模式、工作队列等

7. 本节干货总结

核心考点(面试/自测)

  1. 什么是协程?async/await的作用是什么?
    协程是可以暂停执行的函数,async定义协程,await暂停等待其他协程完成。

  2. FastAPI中同步路由和异步路由的性能区别?
    异步路由配合异步IO可以实现高并发,同步路由会占用线程资源,并发能力较低。

  3. 如何并发执行多个数据库查询?
    使用asyncio.gather同时发起多个查询,等待所有结果返回。

  4. async def路由中执行CPU密集型任务会有什么问题?
    阻塞事件循环,导致所有请求排队。应使用asyncio.to_thread

  5. 为什么不能用requests库而要用httpx.AsyncClient
    requests是同步库,会阻塞事件循环;httpx.AsyncClient是异步非阻塞的。

实际工作应用场景

  • 场景1:数据聚合服务
    前端需要同时获取用户信息、订单列表、消息未读数。后端使用asyncio.gather并发查询三个数据源,响应时间从串行300ms降至100ms。

  • 场景2:第三方API调用
    在用户登录后,并发调用多个外部服务(获取头像、积分、会员等级)来构建用户中心。

  • 场景3:WebSocket实时推送
    使用asyncio.create_task为每个连接维护发送心跳的协程。

  • 场景4:异步文件处理
    接收上传文件后,将处理任务放入后台异步执行(如转码、压缩),立即返回“已接收”。

  • 场景5:高并发秒杀接口
    利用异步非阻塞的特性,结合Redis等异步客户端,处理大量并发请求。


下节课预告

本节课我们深入掌握了异步编程,并能够编写高性能的异步数据库和HTTP调用。现在,我们的API可以高效处理请求了,但还缺少一个关键组件——用户认证与授权。在真实系统中,我们需要区分不同用户,限制其访问资源。

下节课(第17课),我们将学习:

  • JWT令牌生成与验证:使用python-jose
  • 登录接口实现:验证用户名密码,返回access token
  • 依赖注入保护接口:使用Depends校验token,获取当前用户
  • 路由级别权限控制:区分普通用户和管理员角色

让你的API具备完善的安全防护能力!下节课见!


🔗《20节课 FastAPI 从入门到精通》系列课程导航

去订阅

🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Thomas.Sir

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值