曾经的我, 把数据库当成了魔法盒子。
教程都千篇一律: 先创建个 engine, 再搞个 session factory, 然后写个依赖把 session yield 出去。用起来没毛病, 于是我就到处复制粘贴。但为啥要这么做?其实我也没搞明白。
直到有一天, 轮到我写后台任务 (background worker), 这些套路突然就“不灵光”了。我盯着自己的代码, 脑袋一片浆糊:
- 为啥数据库 engine 要一直留在内存里不放?
- 为啥每个请求都得新建 session?
- 既然 API 要“无状态”, 我们还保存啥东西啊?
- S3 算无状态还是有状态?Redis 又算啥?
总感觉哪儿不对劲。
那个挥之不去的矛盾
大家都说: “API 就得无状态!”
但现实中的后端, 总有点啥东西是要保留住的:
- 数据库 engine 一跑就常驻内存
- Session factory 藏 app 状态里
- S3 客户端塞在 context 里
- Redis 连接全局共享
这到底是无状态, 还是有状态?
很长一段时间, 我的脑子就把这归为: “反正大家都这么写。” 其实就是: “虽然我没懂, 但用了也没出啥大问题, 先不管。”
一张图, 理清一切
后来, 我终于找到了让一切豁然开朗的心智模型。
想象一下, 你的系统被一条横线切成两半:
横线以上: 请求处理器和后台任务
- 表现得很“无状态”
- 每次请求都是全新的一轮工作
- 不会偷偷带着上次的数据过来
横线以下: 数据持久化世界
- Postgres 的表和行
- S3 的桶和对象
- Redis 的键值对
- 这些地方, 才是真正的“状态长存”
横线上: 连接两个世界的“桥梁”——客户端对象
- 数据库 engine 和它的连接池
- S3 客户端以及配置
- Redis 客户端和连接管理
- 它们是你无状态代码和有状态世界之间的桥梁
一旦按这张图理解, 所有迷雾都消散了。
你的代码并没有“耍赖”, 只是合理地维护了访问有状态世界的通道。
为什么数据库看起来“格外隆重”?
说实话, 数据库的“仪式感”明显比 S3、Redis 这种服务高多了。这事儿其实有原因。
连接池, 绝对不能马虎
把数据库想象成一家只有 100 个餐桌的餐厅。
如果每个顾客 (请求) 都自己扛一张桌子进来还不带走, 分分钟桌子堆满餐厅。更惨的是, 20 个服务员 (worker), 每人每秒再带 10 张桌子……餐厅直接变桌子批发市场。
正确姿势是这样的:
- 餐厅自带一波桌子 (连接池/engine)
- 顾客吃完就撤, 桌子轮着用
- 下一个顾客直接坐现成的桌子
代码里也是:
- 每个进程一个 engine, 内部管理连接池
- 每个请求从池子里借一根连接
- 请求完活儿, 连接还回池子
这不是写法风格, 这是“活命法则”。
事务, 得有自己的“小黑屋”
数据库的 session 不只是连接, 更是“事务边界”。
想象 session 是你专用的小本本, 想改啥就先写上。最后决定“提交”, 就把本本交上去;要是“反悔”, 直接撕掉。
如果把本本给所有人轮流写, A 以为刚下好订单, B 又觉得刚取消了, 最后数据库懵圈: 你们到底想干嘛?
所以 session 必须一请求一份, 专人专用, 开始到结束都干净利落。
S3 又是个啥角色?
S3 的存在感有点特殊, 和数据库不太一样。
S3 也保存数据啊, 今天传个文件, 明天还能下回来, 妥妥的有状态。
但关键区别在于: 你和 S3 是通过 HTTP 打交道的。每次上传、下载, 都是独立的一锤子买卖。没有“事务”什么的, 没有哪个本本帮你记着还没提交的改动。
拿个生活例子:
- 数据库: 你进档案室, 手里拿着本子修修改改, 写完决定存档还是作废。
- S3: 你寄快递, 每次都是独立一件, 快递公司不会帮你记着上次的内容。
虽然 S3 也有“状态”, 但数据库因为连接和事务的存在, 需要更多的“仪式感”。
“无状态”到底是啥意思?
谜底揭晓时刻到了。
大家说“无状态 API”, 其实不是说你啥都不能放内存里。
真正的意思是: 每个请求都不依赖于上一个请求的“隐形”状态。
也就是说:
- ❌ 别让数据库 session 跨请求存活
- ❌ 别把“当前用户”写进全局变量
- ❌ 别靠某个会变的全局对象来传递数据
- ✅ 可以共享 engine、客户端、配置等基础设施
- ✅ 每个请求新建 session 或 context
- ✅ 状态的读写交给数据库、缓存、存储等外部系统
共享对象只是“工具”, 真正的业务状态应该留在分界线下。
背景任务里, 分界线最关键
这些理论, 等你写后台 worker 的时候才会真切体会。
用 Web 框架时, 框架会帮你把 session 的创建和销毁都打点得妥妥的。你只管写业务逻辑就行。
但一旦到后台 worker, 轮到你自己决定:
- 什么东西是整个 worker 共享的?
- 什么东西是每个任务独立新建的?
这就是分界线的选择题。
一个干净的套路如下:
# worker 初始化时 (长生命周期)
context["db_session_factory"] = SessionFactory(engine)
context["s3_client"] = S3Client(config)
# 每个任务中 (短生命周期)
async def process_document(context, document_id):
session = context["db_session_factory"].create()
s3 = context["s3_client"]
# 用全新 session 干活
# session 自己管理事务
# 用完就丢
工厂和客户端常驻边界, session 和业务逻辑则在分界线上方, 各干各的井水不犯河水。
为什么这个心智模型很重要?
说实话, 没碰过坑之前我也是“教程怎么写我就怎么抄”。
FastAPI 教程说: “这样写。”我就跟着写。Litestar 文档说: “放这儿。”我就塞这儿。
但一旦你开始:
- 多 worker 协同
- 写后台任务
- 混用数据库、S3、Redis、消息队列
- 调试奇怪的连接问题
这时候, 复制粘贴就救不了你。你得明白为啥要这么设计。
对我来说, 真正的转折点是看清了这条分界线:
- 分界线下: 持久化系统 (Postgres、S3、Redis), 业务状态的老巢
- 分界线上: 客户端对象 (engine、session factory、S3 client), 桥梁
- 分界线上方: 无状态 (ish) 请求处理器, 只管干活儿
一旦你脑子里有了这张图, 很多“神秘套路”其实都是有理有据的设计选择。
你不是因为 Python 的什么 bug 才一直留着 engine, 而是因为连接池很贵, 只能共享。
你不是因为教程让你每次新建 session 就照做, 而是因为事务必须有干净的边界, 连接池也需要反复利用连接。
你不是在“自相矛盾”地把 S3 client 共享、数据库 session 独立, 而是认清了不同系统的交互模式各不相同。
总结一下 (TL;DR)
如果你以后忘了细节, 只想回来看核心观点:
无状态不是“啥都不能共享”, 而是“每个请求都不依赖以前的隐形状态”。
分界线心智图:
- 下方: 持久化系统 (数据库、S3、Redis)
- 线上: 客户端基础设施 (engine、session factory、client)
- 上方: 每请求独立的业务逻辑 (session、service、handler)
数据库之所以显得特殊, 是因为:
- 连接池有硬性上限
- 事务要求干净的边界
- session 记录着变更, 直到 commit/rollback
实践中:
- 共享: engine、客户端、配置、工厂
- 独立新建: session、请求上下文、事务边界
以后再看到 engine、sessionmaker、Session 或 s3_client, 你就知道它们该在架构的哪一层, 为什么要那样设计。
这些不是魔法套路, 而是你有意识地划清了你和有状态世界之间的那一道线。
(本文由人类撰写, 部分内容借助 AI 优化。)
