# 无状态API与有状态数据库的分界线 曾经的我,把数据库当成了魔法盒子。 教程都千篇一律:先创建个 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 共享的? - 什么东西是每个任务独立新建的? 这就是分界线的选择题。 一个干净的套路如下: ```python # 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、消息队列 - 调试奇怪的连接问题 这时候,复制粘贴就救不了你。你得明白**为啥**要这么设计。 对我来说,真正的转折点是看清了这条分界线: 1. **分界线下**:持久化系统(Postgres、S3、Redis),业务状态的老巢 2. **分界线上**:客户端对象(engine、session factory、S3 client),桥梁 3. **分界线上方**:无状态(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 优化。)