Skip to content

FastAPI 依赖:资源生命周期、线程与事务边界 ​

前置:输入契约、with与异常。本篇依据FastAPI0.121.0的scope能力解释费用账本依赖。示例未执行,尤其未验证线程调度、取消或异常路径;先把资源归属写清楚,再写yield。

Depends 表达调用关系 ​

FastAPI的Depends是否等于全局单例?Depends 构造请求内的依赖关系,同一请求的相同依赖默认有缓存;它不自动创建进程单例,也不意味着多个请求共享同一数据库连接。本例每个请求调用 get_db 产生一个连接,路由只接收已解析的 Database 参数。

共享的是 create_app 闭包中的数据库文件路径字符串;连接不是 app.state 上的全局对象。依赖可能在业务路由之前失败,例如目录不可写;此时不能执行写入。use_cache=False 可影响同一请求重复调用,但不会让错误的全局连接设计安全。调用次数、作用域和资源所有权应分别推理,不能用“依赖注入”四字替代设计。

Python闭包局部类型别名 Database 在本例函数定义时解析;源码没有启用 postponed annotations,避免字符串注解在模块全局解析时找不到闭包变量。复制示例时若加 from future import annotations,应同时检查 FastAPI 如何解析这个别名,不能把语法成功当成路由注册成功。

yield 与 function 作用域 ​

FastAPI的yield依赖何时关闭资源?本例用 Depends(get_db, scope="function"),连接在 yield 前建立,路由完成后通过 finally 关闭;function作用域的退出发生在响应发送之前。此能力从0.121.0引入,不能拿旧版本教程中的退出时机替代当前契约。

get_db 的结构是 connect、try/yield、finally/close。成功、HTTPException和未捕获异常都要走finally;拿掉finally便会把连接释放依赖于垃圾回收。默认request作用域的yield依赖与流式响应关系不同,退出在响应发送完成之后,见官方高级依赖说明。关闭和提交不混在一起:这里只 close,不能在yield之后偷偷COMMIT。

本例返回的是已物化的模型、字典或列表,不流式读取游标,因此可提前关闭。若改成StreamingResponse并在生成器迭代时才访问db,function作用域会关闭过早;要重新设计流消费期间的依赖及取消清理,不能仅删finally。后台任务同样不能保存请求连接,必须拥有自己的资源和失败策略。

同步依赖与线程身份 ​

FastAPI同步依赖和同步路由一定在同一线程吗?线程池执行不承诺线程身份一致;SQLite默认check_same_thread=True会限制创建线程,不能假设同步依赖的创建和路由使用恰好同线程。本例显式check_same_thread=False,并严格限定每连接只供一个请求串行使用。

关闭线程身份检查不等于提供互斥,更不允许多个协程或后台任务同时操作同一连接。请求A和B各有连接,SQLite锁负责它们之间的写入协调;同一个请求不再启动并行数据库任务。若要连接保持默认线程检查,可改成整个同步业务函数内部创建、使用并关闭连接,但这样依赖只应传路径或工厂,不直接交付连接。

同步函数不会神奇变成非阻塞驱动。FastAPI把普通def路由及普通def依赖放入线程池;你在async def内直接调用普通工具函数时,框架不会自动把该工具调用也挪到线程池。本系列选择普通def路由配阻塞sqlite3,见官方并发说明。

提交属于业务事务 ​

FastAPI为什么不在依赖清理阶段提交事务?清理阶段的提交可能让业务返回与提交失败错位;本例在路由内BEGIN IMMEDIATE、检查约束、构造输出模型、COMMIT,异常时ROLLBACK。事务成功之后再返回已验证的结果,finally只做资源释放。

创建先查重复reference,再查记录数,最后INSERT,三步位于同一个写事务。BEGIN IMMEDIATE在检查前争取写锁,避免两个请求都看见99条然后同时添加越过100。唯一索引是重复参考号的数据库防线,不能单靠应用先查。若有其他程序直接写数据库,100条这一应用级限制可能被绕过;本例数据库只供该API写入,已有数据库还须满足同一schema。

BEGIN失败发生在try之外,因为尚未进入事务;SQLITE_BUSY由错误处理器映射503。进入事务后异常分支检查in_transaction才ROLLBACK,避免提交失败或其他路径中无事务还回滚。except Exception在这里仅用于回滚后重抛,绝不吞掉错误。系统中断、进程崩溃和客户端丢失连接有更复杂的结果,不能保证客户端看到失败时数据一定没提交。

应用启动与测试隔离 ​

FastAPI的lifespan负责哪些初始化?本例在lifespan启动阶段建表并关闭启动连接,请求阶段另建连接;关闭应用时没有遗留的全局数据库连接。小型同步DDL只阻塞启动阶段,不把它描述为异步数据库访问。

create_app接受数据库路径,测试用临时目录真实文件,避免:memory:在不同连接中各有独立数据库。工厂不在导入时创建数据库,TestClient必须作为上下文管理器进入才能执行lifespan。路径父目录需要已存在;服务工作目录决定默认ledger.sqlite3的位置。该建表不是迁移系统:CREATE TABLE IF NOT EXISTS不能修复已有表结构、增加约束或清洗历史坏数据。

SQLite连接上下文管理器主要处理事务提交/回滚,不负责自动close;本例显式finally关闭,见Python3.12 sqlite3文档。isolation_level=None使用显式SQL控制事务,本例不混用3.12新autocommit模式与隐式事务假设。

练习 ​

  1. 将连接改成模块级变量,同时保留check_same_thread=False,指出会破坏哪些保证。
  2. 在yield之后COMMIT,再在INSERT后立即return,列出提交失败会发生在哪个阶段。
  3. 用function作用域连接生成流式响应,为什么在开始迭代游标时可能已经关闭?
  4. 将测试数据库换成:memory:,为什么建表成功仍可能找不到expenses?

参考答案 ​

全局连接会让请求交错事务、一个请求关闭另一个的资源;关闭线程检查没有解决共享。清理提交把错误推迟到路由业务已完成之后,难以维持输出与状态一致,应在业务边界提交。function作用域先退出、流式迭代后消费,需调整资源所有者及作用域。内存数据库属于单个连接,启动连接和请求连接不共享;本项目直接拒绝该路径,测试使用文件。验证这些推断需要实际运行对应生命周期测试,本轮只完成静态分析。

继续阅读