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