FastAPI 中如何管理数据库连接和 Session?
简化版
FastAPI 通常用依赖注入管理数据库 Session:每个请求创建一个 Session,处理完成后关闭;应用启动时可以初始化连接池,但不要多个请求共享同一个 Session。同步 ORM 用同步依赖,异步 ORM 或异步驱动则配合 async 依赖。
详细版
数据库管理要区分连接池和 Session:
- 连接池是应用级资源,可以在应用生命周期中创建;
- Session 是请求级资源,通常每个请求独立创建;
- 请求结束后必须关闭 Session;
- 事务要明确 commit、rollback;
- 同步 SQLAlchemy 不要直接放进
async def中长时间阻塞事件循环; - 异步 SQLAlchemy 要使用
AsyncSession和异步驱动。
典型依赖:
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
路由中:
def list_users(db: Session = Depends(get_db)):
return db.query(User).all()
完整版教学
一、为什么数据库 Session 不能随便全局共享
数据库访问是 Web 项目最容易出问题的地方之一。很多初学者会把数据库连接或 Session 做成全局变量,然后所有请求都复用同一个对象。这在并发场景下很危险。
以 SQLAlchemy Session 为例,它不只是一个“连接”。它还维护了事务状态、对象缓存、脏数据跟踪等上下文。多个请求共享一个 Session,可能导致事务互相影响、数据状态污染、并发安全问题。
正确思路是:连接池可以是全局或应用级资源,但 Session 应该是请求级资源。每个请求拿一个 Session,用完关闭。
二、同步 SQLAlchemy 的典型写法
同步 SQLAlchemy 项目里常见结构是:
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
engine = create_engine(DATABASE_URL, pool_pre_ping=True)
SessionLocal = sessionmaker(bind=engine, autocommit=False, autoflush=False)
然后用依赖注入管理 Session:
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
接口中声明:
@app.get("/users")
def list_users(db: Session = Depends(get_db)):
return db.query(User).all()
yield 前创建 Session,路由执行时使用 Session,路由结束后关闭 Session。这是 FastAPI 中非常经典的数据库依赖模式。
三、事务如何处理
对于写操作,事务边界要清楚:
@app.post("/users")
def create_user(user_in: UserCreate, db: Session = Depends(get_db)):
user = User(**user_in.model_dump())
db.add(user)
db.commit()
db.refresh(user)
return user
如果业务复杂,可以在 service 层统一处理 commit 和 rollback。无论放在哪里,都要避免“有些地方 commit,有些地方忘记 commit”的混乱状态。
更稳的写法是在依赖中加入异常回滚:
def get_db():
db = SessionLocal()
try:
yield db
db.commit()
except Exception:
db.rollback()
raise
finally:
db.close()
但这种写法意味着所有使用该依赖的接口都会在成功后 commit,是否合适要看项目风格。有些团队更喜欢业务代码显式 commit,因为事务边界更明显。
四、同步 ORM 和 async def 的关系
如果使用同步 SQLAlchemy,却把路由写成 async def,并在里面直接执行同步查询,查询期间会阻塞事件循环。并发高时会影响其他异步请求。
在同步 ORM 项目中,可以使用普通 def 路由,让 FastAPI 将其放入线程池:
@app.get("/users")
def list_users(db: Session = Depends(get_db)):
return db.query(User).all()
如果项目希望全链路异步,就应该使用异步数据库驱动和 AsyncSession,而不是简单把函数改成 async def。
五、异步 SQLAlchemy 的写法
异步 SQLAlchemy 通常使用 create_async_engine 和 async_sessionmaker:
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
engine = create_async_engine(ASYNC_DATABASE_URL)
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)
async def get_db():
async with AsyncSessionLocal() as session:
yield session
查询也要用 await:
@app.get("/users")
async def list_users(db: AsyncSession = Depends(get_db)):
result = await db.execute(select(User))
return result.scalars().all()
异步数据库并不意味着所有场景都更快。它主要改善 I/O 等待期间的并发能力,同时也会引入更高的编程复杂度。
六、连接池配置要关注什么
生产环境要关注连接池大小、连接回收、连接健康检查和数据库最大连接数。Web worker 数量乘以每个进程的连接池大小,可能很快超过数据库承受能力。
例如 4 个 worker,每个 worker 连接池最多 20 个连接,就可能占用 80 个数据库连接。数据库如果最大连接数只有 100,再加上后台任务和运维连接,就容易出问题。
因此数据库连接池不是越大越好,要结合数据库能力、接口耗时、并发量和 worker 数量设置。
七、常见误区与追问
| 对象 | 能否全局复用 | 原因 |
|---|---|---|
| Engine / 连接池 | 可以作为应用级资源 | 内部管理连接复用,创建成本较高 |
| Session / AsyncSession | 不应该跨请求共享 | 保存事务状态、对象状态和上下文 |
| ORM Model 实例 | 不建议长期跨请求持有 | 可能绑定旧 Session 或携带过期状态 |
4 个 worker * 每个 pool_size=10 + max_overflow=20
理论峰值连接 = 4 * (10 + 20) = 120
如果数据库 max_connections=100,就可能在压测或流量峰值时耗尽连接。
记忆钩子:连接池是“公用水库”,Session 是“一次请求的一只水杯”,水杯不要多人共用。
第一个误区是全局共享 Session。连接池可以复用,Session 不应该跨请求复用。
第二个误区是忘记关闭 Session。长时间运行后连接池会被耗尽。
第三个误区是在异步接口里混用同步数据库驱动。代码看起来异步,实际仍然阻塞。
第四个误区是把 ORM 模型直接作为响应模型。ORM 模型代表数据库结构,外部 API 应该用单独的 Pydantic 输出模型。
- 误区:全局共享 SQLAlchemy Session 可以减少开销。 Session 保存事务、缓存和对象状态,跨请求共享会造成数据污染和并发安全问题。
- 误区:关闭 Session 等于关闭数据库连接池。
db.close()通常是把连接归还连接池,而不是销毁整个 Engine;这正是请求级 Session 可频繁创建关闭的原因。 - 误区:
async def路由里用同步 ORM 也没问题。 同步查询会阻塞事件循环;要么用普通def路由,要么切到异步驱动和AsyncSession。 - 追问:事务 commit 应该放在哪里? 简单项目可在路由或 service 显式 commit;统一依赖自动 commit 也可行,但要保证事务边界清晰并正确 rollback。
- 追问:连接池大小怎么估算? 至少要把 worker 数量、每个进程 pool_size、max_overflow、后台任务连接和数据库上限一起算,不能只看单进程配置。
- 追问:为什么响应模型不要直接用 ORM 模型? ORM 模型代表表结构,可能泄漏内部字段,并让 API 契约和数据库演进强耦合。
八、加强记忆
记 FastAPI 数据库管理:连接池是应用级,Session 是请求级;Session 用 Depends + yield 创建和关闭;同步 ORM 配普通 def 更稳,异步 ORM 要使用 AsyncSession 和异步驱动;事务边界要明确,连接池大小要结合 worker 数量一起算。