别再用 `@app.on_event` 了:FastAPI `lifespan` 才是初始化资源的正确姿势

一、先讲个我踩过的坑

去年接手一个 FastAPI 项目,线上偶尔报数据库连接泄漏。查了半天,发现是这么写的:

@app.on_event("startup") async def startup(): await db.connect() @app.on_event("shutdown") async def shutdown(): await db.disconnect()

看起来没毛病对吧?启动时连数据库,关闭时断开。但问题出在:如果startup执行到一半挂了,shutdown根本不会跑。

比如数据库连到一半超时了,前面已经建立的几个连接就成了孤儿,没人关。次数多了连接池就满了,然后整个服务假死。

更坑的是,FastAPI 从 0.93 版本开始把on_event标记为 deprecated 了,官方明确推荐用lifespan代替。我当时改完之后,那个泄漏问题再也没出现过。

所以今天这篇,就是想把lifespan给你讲明白,别再走我走过的弯路。


二、lifespan到底是个啥

简单说,lifespan就是一个异步上下文管理器,它包住了整个应用的生命周期。应用启动前进入yield之前的代码,应用关闭后执行yield之后的代码。

from contextlib import asynccontextmanager from fastapi import FastAPI @asynccontextmanager async def lifespan(app: FastAPI): # 启动时执行 print("应用启动了,初始化资源...") await db.connect() yield # 应用在这里运行 # 关闭时执行 print("应用关闭,清理资源...") await db.disconnect() app = FastAPI(lifespan=lifespan)

就这么简单。yield把代码分成两半:上面是启动逻辑,下面是清理逻辑。

你可能会问,这跟try/finally有啥区别?本质上就是try/finally的语法糖。但关键在于:不管启动过程中有没有异常,yield后面的清理代码都会执行。这就解决了我前面说的那个泄漏问题。


三、实际项目中怎么用

场景 1:数据库连接池

这是最常见的用法。以 SQLAlchemy 异步引擎为例:

from contextlib import asynccontextmanager from fastapi import FastAPI from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession from sqlalchemy.orm import sessionmaker engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/db") AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) @asynccontextmanager async def lifespan(app: FastAPI): # 启动时创建连接池 async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) yield # 关闭时释放连接池 await engine.dispose() app = FastAPI(lifespan=lifespan)

注意一个细节:我在启动时建表了。如果你用 Alembic 做迁移,建表这步可以省掉,别混着用,不然容易出问题。

场景 2:Redis 连接

import redis.asyncio as redis @asynccontextmanager async def lifespan(app: FastAPI): app.state.redis = await redis.from_url("redis://localhost", decode_responses=True) yield await app.state.redis.close()

这里我把 redis 客户端挂到了app.state上,这样在路由里就能通过request.app.state.redis拿到。比全局变量干净,测试的时候也好 mock。

场景 3:加载机器学习模型

这个场景太典型了。模型加载慢,不能每次请求都加载一次,必须在启动时预加载:

import joblib @asynccontextmanager async def lifespan(app: FastAPI): # 启动时加载模型(这步可能要好几秒) app.state.model = joblib.load("model.pkl") app.state.vectorizer = joblib.load("vectorizer.pkl") print("模型加载完成") yield # 模型一般不需要特别清理,但可以在这里释放显存 del app.state.model app = FastAPI(lifespan=lifespan)

路由里用的时候:

from fastapi import Request @app.post("/predict") async def predict(request: Request, text: str): model = request.app.state.model vectorizer = request.app.state.vectorizer features = vectorizer.transform([text]) result = model.predict(features) return {"result": result.tolist()}

场景 4:多个资源一起管理

真实项目不可能只初始化一个东西。数据库、Redis、MQ、模型…… 全塞在一个lifespan里也行,但写多了就乱。我的做法是拆成多个上下文管理器,然后用AsyncExitStack组合:

from contextlib import asynccontextmanager, AsyncExitStack @asynccontextmanager async def init_db(app): await db.connect() yield await db.disconnect() @asynccontextmanager async def init_redis(app): app.state.redis = await redis.from_url("redis://localhost") yield await app.state.redis.close() @asynccontextmanager async def lifespan(app: FastAPI): async with AsyncExitStack() as stack: await stack.enter_async_context(init_db(app)) await stack.enter_async_context(init_redis(app)) yield

AsyncExitStack的好处是:前面的资源初始化成功了,后面某个失败了,已经成功的那些会自动按逆序清理。不会出现半初始化的僵尸状态。


四、新手容易踩的几个坑

坑 1:在lifespan里做耗时的同步操作

# 错误写法 @asynccontextmanager async def lifespan(app): model = load_big_model() # 这个函数是同步的,要跑 10 秒 yield

如果load_big_model()是个同步阻塞函数,它会卡住整个事件循环。这期间应用没法处理任何请求,甚至健康检查都超时,K8s 可能直接把你 pod 杀了。

解决办法是扔到线程池里跑:

import asyncio @asynccontextmanager async def lifespan(app): model = await asyncio.to_thread(load_big_model) app.state.model = model yield

坑 2:yield后面的代码不执行

有个新手同事问我:" 我在yield后面写了打印,怎么从来没看到过?"

原因是他用uvicorn启动后,直接Ctrl+C强杀了进程。强杀的情况下 Python 来不及执行清理逻辑就退出了。正常的SIGTERM信号(比如docker stop、k8s 滚动更新)是会触发lifespan关闭逻辑的,但kill -9不会。

所以别把lifespan的清理当成 100% 可靠的兜底,该做的幂等和超时还是要做。

坑 3:把lifespan当 middleware 用

有人在lifespan里写请求级别的逻辑,比如统计请求数。这是不对的。lifespan只在应用启动和关闭时各执行一次,不是每个请求都跑。请求级别的逻辑用 middleware 或依赖注入。

简单区分:

  • 应用级资源(连接池、模型、配置)→lifespan
  • 请求级逻辑(鉴权、日志、计时)→ middleware / Depends

坑 4:测试的时候忘了lifespan

用TestClient测试的时候,默认是不会触发lifespan的。你需要这样:

from fastapi.testclient import TestClient with TestClient(app) as client: # 进入 with 时触发 startup,退出时触发 shutdown response = client.get("/") assert response.status_code == 200

用with包住,lifespan才会正常执行。不然你测试里访问request.app.state.redis会报AttributeError。


五、跟旧方式的对比

最后做个总结,为啥要从on_event迁到lifespan:

表格

对比项@app.on_eventlifespan
状态已废弃(deprecated)官方推荐
异常安全启动失败不触发 shutdown保证清理代码执行
资源共享靠全局变量可挂app.state,更规范
多资源管理多个事件函数,顺序不可控AsyncExitStack组合,顺序明确
测试支持需手动触发TestClient原生支持

迁移也很简单,把startup里的代码搬到yield前面,shutdown里的搬到yield后面,完事。


写在最后

lifespan不是什么高深的东西,本质就是个上下文管理器。但它解决了 FastAPI 项目里一个很实际的问题:资源的初始化和清理怎么写才安全、才优雅。

我见过太多项目图省事,把数据库连接写成全局变量,启动时连一下就不管了。平时跑着没事,一到发布、重启、异常的时候就出幺蛾子。写代码嘛,还是得把这些 "边角料" 处理好,线上才能少熬夜。

如果你项目里还在用on_event,建议找个时间迁一下,半小时的事,能省掉后面很多麻烦。

相关新闻

基于SpringBoot的教学辅助平台设计与开发实践

基于SpringBoot的教学辅助平台设计与开发实践

1. 项目概述做毕业设计选题时,很多同学在“管理系统”和“平台”之间反复纠结,最后选来选去还是落到了老一套的增删改查上。今天想跟你好好聊聊我折腾过的一个选题——基于SpringBoot技术的教学辅助平台。如果你正愁毕设不知道做什么、或者担心做出来太像…

2026/10/21 9:30:31 阅读更多 →
不愧是从大厂挖来的项目经理,这工作流太恐怖

不愧是从大厂挖来的项目经理,这工作流太恐怖

公司最近新来了个项目经理,听说是从大厂挖来的。 入职半个月,我就发现她确实有点不一样。 老板上午临时丢过来一个新项目,需求还没完全讲清楚,下午她就已经把项目目标、任务拆分、负责人和时间节点整理好了。第二天再看&#xff…

2026/10/22 13:56:56 阅读更多 →
无人机PID调参新思路:非线性增益调度与元启发式优化

无人机PID调参新思路:非线性增益调度与元启发式优化

做无人机飞控这件事,调PID是个绕不开的环节。我见过不少开发者在初期把一个参数一个参数地试,试到后面自己都不记得哪组组合有效,更麻烦的是,换一个飞行工况可能又得从头再来。某个项目里我接手一台悬停性能很不稳定的四旋翼&…

2026/10/21 9:30:31 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年,我大部分时间都在跟浏览器标签页打交道,直到第一次把桌面应用跑起来,才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词,最开始是在某段时间频繁出现在同事的讨论里,一个用HTML…

2026/10/22 10:18:38 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介:亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档,主要面向智能锁安装人员、终端用户及售后维护者,帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件,大小4.37MB,内容结构清晰&#xff…

2026/10/22 11:01:53 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统,整车要求空载跑到8m/s,电机转速已经拉到额定点,驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令,转速纹丝不动,三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/10/22 9:44:47 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年,用它写过嵌入式驱动、做过游戏服务端,也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词,我其实挺感慨的——这门语言的难点从来不是语法多,而是知识点之间的耦合太深…

2026/10/22 10:42:00 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介:亚太天能T109滑盖智能锁产品说明书以PDF格式发布,适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开,先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能,再给出完整安装步骤&…

2026/10/22 10:40:08 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历:翻开源码注释、论文摘要或者一份专业报告,每个字都认识,连在一起却像天书。想找人聊聊,身边要么没人懂,要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型,几秒钟之后&a…

2026/10/22 10:56:37 阅读更多 →

日新闻

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/22 1:47:45 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/21 21:32:28 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/22 5:12:39 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/21 21:50:56 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/21 21:51:55 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/21 16:24:48 阅读更多 →