Featured image of post 无状态API与有状态数据库的分界线
技术 Web开发

无状态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 共享的?
  • 什么东西是每个任务独立新建的?

这就是分界线的选择题。

一个干净的套路如下:

# 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、请求上下文、事务边界

以后再看到 enginesessionmakerSessions3_client, 你就知道它们该在架构的哪一层, 为什么要那样设计。

这些不是魔法套路, 而是你有意识地划清了你和有状态世界之间的那一道线。


(本文由人类撰写, 部分内容借助 AI 优化。)

© 2022 - 2026 张欣耕

保留所有权利