曾经的我,把数据库当成了魔法盒子。
教程都千篇一律:先创建个 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 优化。)
