codecamp

4.3 理解请求内缓存与依赖执行方式

理解请求内缓存与依赖执行方式

依赖复用有两个容易混淆的概念:FastAPI 默认会在一次请求内复用同一个依赖调用的结果;依赖函数本身是同步还是异步,又决定了它以什么方式执行。这种缓存不是 Redis,也不会把结果带到下一个请求。同步依赖也不会因为“很短”就自动在事件循环里运行。

本节用计数器验证请求范围的缓存,用 use_cache=False 对照强制重新执行的情况,再观察同步和异步依赖的执行线程。计数器只为测试行为服务,不能作为生产缓存实现。

默认缓存的范围

下面的 /cached 路由两次声明同一个 load_context 依赖。由于它们使用的是同一个依赖调用并采用默认的 use_cache=True,一次请求只会调用一次函数,并把同一个结果注入两个参数。第二次 HTTP 请求会重新执行依赖。

use_cache=False 的含义更窄:在当前请求的依赖解析中,不复用这个依赖已经得到的结果。它不会关闭其他请求的缓存,也不会把结果保存到应用生命周期中。下面的 /uncached 路由用两个 use_cache=False 参数验证同一请求内会执行两次。

<!-- file: ch04_cache/dependency_cache_demo.py -->

from __future__ import annotations


import threading
from typing import Annotated, Any


from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient




context_calls = 0
sync_calls = 0
async_calls = 0




async def load_context() -> dict[str, int]:
    """模拟轻量的请求上下文解析,只用于观察调用次数。"""
    global context_calls
    context_calls += 1
    return {"call": context_calls}




RequestContext = Annotated[dict[str, int], Depends(load_context)]
UncachedContext = Annotated[
    dict[str, int], Depends(load_context, use_cache=False)
]


app = FastAPI(title="依赖缓存与执行方式示例")




@app.get("/cached")
async def cached(
    first: RequestContext,
    second: RequestContext,
) -> dict[str, Any]:
    return {
        "first": first,
        "second": second,
        "same_result": first is second,
        "context_calls": context_calls,
    }




@app.get("/uncached")
async def uncached(
    first: UncachedContext,
    second: UncachedContext,
) -> dict[str, Any]:
    return {
        "first": first,
        "second": second,
        "same_result": first is second,
        "context_calls": context_calls,
    }




def sync_dependency() -> str:
    """没有等待操作的同步依赖,只为观察执行位置。"""
    global sync_calls
    sync_calls += 1
    return f"sync:{threading.get_ident()}"




async def async_dependency() -> str:
    global async_calls
    async_calls += 1
    return f"async:{threading.get_ident()}"




SyncValue = Annotated[str, Depends(sync_dependency)]
AsyncValue = Annotated[str, Depends(async_dependency)]




@app.get("/dependency-kinds")
async def dependency_kinds(
    sync_value: SyncValue,
    async_value: AsyncValue,
) -> dict[str, Any]:
    return {
        "route_thread": threading.get_ident(),
        "sync_value": sync_value,
        "async_value": async_value,
        "sync_calls": sync_calls,
        "async_calls": async_calls,
    }




def self_check() -> None:
    global context_calls, sync_calls, async_calls
    context_calls = 0
    sync_calls = 0
    async_calls = 0


    with TestClient(app) as client:
        first_request = client.get("/cached")
        second_request = client.get("/cached")
        uncached_request = client.get("/uncached")
        kinds_request = client.get("/dependency-kinds")


    assert first_request.status_code == 200
    assert first_request.json()["same_result"] is True
    assert first_request.json()["context_calls"] == 1


    assert second_request.status_code == 200
    assert second_request.json()["same_result"] is True
    assert second_request.json()["context_calls"] == 2


    assert uncached_request.status_code == 200
    assert uncached_request.json()["same_result"] is False
    assert uncached_request.json()["context_calls"] == 4


    kinds = kinds_request.json()
    assert kinds_request.status_code == 200
    assert kinds["sync_calls"] == 1
    assert kinds["async_calls"] == 1
    assert kinds["sync_value"].startswith("sync:")
    assert kinds["async_value"].startswith("async:")
    sync_thread = int(kinds["sync_value"].split(":", 1)[1])
    async_thread = int(kinds["async_value"].split(":", 1)[1])
    assert sync_thread != kinds["route_thread"]
    assert async_thread == kinds["route_thread"]


    print("dependency_cache_demo self-check passed")
    print(
        "sync dependency and async dependency thread observations:",
        kinds["sync_value"],
        kinds["async_value"],
        "route:",
        kinds["route_thread"],
    )




if __name__ == "__main__":
    self_check()

运行检查:

python ch04_cache/dependency_cache_demo.py

关键断言表示:

  • 第一次 /cached 请求中,两个参数拿到同一个结果,计数器从 0 变为 1。
  • 第二次 /cached 请求重新解析依赖,计数器变为 2;上一个请求的结果没有泄漏进来。
  • /uncached 的两个参数都设置了 use_cache=False,同一请求内产生两次调用,计数器变为 4。

示例中的缓存判断限定在同一个依赖调用、同一个请求内。FastAPI 的依赖图还会根据依赖的调用关系和参数参与解析;不要把本节现象扩大成“所有参数相同的函数都会全局缓存”。如果业务需要跨请求缓存,应明确设计数据失效、并发一致性和错误回退,再选择数据库、Redis 或其他缓存工具。

同步依赖和异步依赖怎么选

async_dependency() 在当前异步请求链中直接执行;sync_dependency() 是普通 def,FastAPI 会把它放到 Starlette 使用的线程池中执行。示例返回线程 ID 只是帮助观察这个事实,线程 ID 和具体数量不应被写成业务契约。

对于只读取请求头、比较一个数字或做其他轻量且不阻塞的检查,异步依赖可以避免一次不必要的线程调度。这里的“异步”不等于必须包含 await;它表示该依赖符合当前异步调用链的执行方式。

如果依赖内部调用同步数据库驱动、同步 SDK 或文件系统操作,把它改名为 async def 不会使阻塞消失。应按照第二章的规则使用明确的线程池边界,或者选择该库真正的异步客户端。相反,已经是异步 I/O 的依赖要在异步函数中 await 它,不能用同步包装把它重新送进线程池。

同样不要为了追求“全都 async”把 CPU 密集型计算塞进事件循环。依赖的形式只解决依赖解析方式,不能改变底层工作类型;计算任务仍应根据耗时和吞吐要求选择进程、任务系统或其他隔离方案。

把缓存和副作用分开

依赖可能被多个路由参数复用,但不要依赖“只执行一次”来承载必须发生的副作用。例如写审计日志、扣减库存或发送通知都不应藏在一个默认缓存的依赖中,否则调用方修改依赖图后,副作用次数可能改变。读取上下文、加载资源和检查权限适合成为可复用依赖;写操作应由明确的 service 或任务边界负责,并留下独立的测试。

资料来源

4.2 串联身份与资源所有权检查
4.4 用一致的资源路径支持依赖复用
温馨提示
下载编程狮App,免费阅读超1000+编程语言教程
取消
确定
目录

关闭

MIP.setData({ 'pageTheme' : getCookie('pageTheme') || {'day':true, 'night':false}, 'pageFontSize' : getCookie('pageFontSize') || 20 }); MIP.watch('pageTheme', function(newValue){ setCookie('pageTheme', JSON.stringify(newValue)) }); MIP.watch('pageFontSize', function(newValue){ setCookie('pageFontSize', newValue) }); function setCookie(name, value){ var days = 1; var exp = new Date(); exp.setTime(exp.getTime() + days*24*60*60*1000); document.cookie = name + '=' + value + ';expires=' + exp.toUTCString(); } function getCookie(name){ var reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)'); return document.cookie.match(reg) ? JSON.parse(document.cookie.match(reg)[2]) : null; }