数据库基础体系 · 第 9/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。
数据库连接与连接池:容量、超时、排队、泄漏和故障恢复
数据库连接通常被当作一个简单对象:打开连接,执行 SQL,关闭连接。但在生产系统中,一条连接同时占用数据库进程或线程、内存、网络套接字、事务状态、锁和临时资源。应用实例增多、查询变慢或数据库发生切换时,连接问题会表现为:
- 数据库连接数达到上限;
- 请求大量等待连接池;
- 应用线程耗尽,但数据库看起来并不繁忙;
- 事务长期保持打开,阻塞清理或持有锁;
- 数据库已恢复,应用仍不断使用失效连接;
- 重试和连接重建形成“重连风暴”。
理解这些现象,不能只记住“设置一个连接池大小”。需要先区分连接、连接池、数据库容量、排队、超时和事务生命周期,再沿着正常路径和故障路径分析。
一、连接到底是什么
1. 逻辑连接与物理连接
应用看到的“连接”通常是一个客户端对象。它至少包含:
- 到数据库服务器的 TCP 或 Unix socket;
- 数据库协议会话;
- 身份认证和权限上下文;
- 服务端保存的会话状态;
- 客户端驱动保存的读写缓冲区和协议状态。
连接池中的一个对象通常对应一个物理数据库连接。应用请求从池中借出的则是一个租约或借用关系:
请求
└─ 从连接池借出物理连接
└─ 开始或复用事务
└─ 执行 SQL
└─ 提交/回滚
└─ 将连接归还连接池
归还连接不等于关闭物理连接。连接池通常会保留它,供下一个请求复用。
因此必须区分:
- 关闭连接:释放数据库会话和网络资源;
- 归还连接:结束本次借用,物理连接可能仍保留;
- 提交事务:结束当前事务;
- 回滚事务:撤销当前事务并结束事务;
- 释放锁:通常随事务提交或回滚发生,但具体资源还可能受语句和会话状态影响。
如果应用只把连接放回池中,却没有结束事务,下一次请求可能拿到一个仍处于事务中的连接。这是连接池中最危险的状态污染之一。
2. 数据库连接不是“执行 SQL 的瞬时句柄”
数据库服务器通常需要为每个会话保存:
- 当前用户、数据库和权限上下文;
- 当前事务和快照;
- 预处理语句;
- 会话变量;
- 临时表;
- 游标;
- 未读结果;
- 锁和其他事务资源;
- 日志、统计或复制相关状态。
PostgreSQL 的服务端连接通常由一个后端进程处理;MySQL 的连接模型和线程处理方式由其服务器实现与线程配置决定。这里不应把“一个连接等于固定一个操作系统线程”当作跨产品规范,但可以确定的是:连接数增加会消耗数据库的并发处理能力和内存。
一个连接池不能把数据库能承受的并发查询凭空变大。它主要解决的是:
- 复用认证和连接建立成本;
- 限制应用侧同时进入数据库的请求数;
- 在短暂请求高峰期间提供有限排队;
- 统一连接生命周期管理。
它不能解决:
- SQL 本身太慢;
- 锁竞争;
- 索引缺失;
- 数据库内存不足;
- 数据库无法处理所配置的总连接数。
二、连接的生命周期与连接池状态
一个连接池至少需要处理以下状态:
创建中 ──成功──> 空闲
创建中 ──失败──> 失效
空闲 ──借出──> 使用中
使用中 ──归还且健康──> 空闲
使用中 ──归还但污染/失效──> 关闭
空闲 ──空闲超时──> 关闭
空闲 ──健康检查失败──> 关闭
应用请求还存在一个独立状态:
等待借连接 ──获得连接──> 执行 SQL
等待借连接 ──超时──────> 请求失败
执行 SQL ───超时──────> 查询取消或连接失效
“等待借连接”和“等待数据库执行”不是同一件事。它们必须分别测量和设置超时。
1. 借出、使用和归还
伪代码可以表示一个正确的最小生命周期:
conn = pool.acquire(timeout=200ms)
if conn == TIMEOUT:
return 503
try:
begin transaction
execute SQL
commit
catch error:
rollback if transaction state is known
mark conn invalid if protocol state is uncertain
raise
finally:
pool.release(conn)
这里有三个关键条件:
finally必须覆盖所有异常路径,包括超时、取消和序列化异常;- 事务结束后才能归还健康连接;
- 如果客户端不知道服务端是否已经执行了请求,不能仅凭“回滚”就断言连接仍然可复用。
实际代码还需要处理结果集关闭、游标关闭、批处理对象释放以及连接重置。不同语言和驱动的 API 名称不同,但生命周期原则相同。
2. 为什么“归还前重置”很重要
请求可能修改会话状态。例如:
- PostgreSQL 的
SET search_path、SET ROLE、临时表、预处理语句; - MySQL 的会话变量、字符集、SQL mode、临时表;
- 当前事务隔离级别;
- 当前数据库或 schema 上下文;
- 未完成的结果读取。
如果这些状态被下一个用户请求继承,就可能造成数据访问错误,甚至权限边界失效。
连接池可以采用两种方式:
- 每次借出前显式设置所需状态;
- 归还时执行驱动或协议提供的重置操作。
重置操作不是万能的。它本身可能产生网络往返,也不一定覆盖应用自定义资源。发生协议错误、连接中断、未读结果或事务状态未知时,通常应该销毁连接,而不是强行重置后复用。
三、容量:连接池大小不是越大越好
1. 三个不同的容量上限
至少要区分:
应用实例并发请求数
连接池最大物理连接数
数据库允许的最大连接数
假设:
- 应用有 8 个实例;
- 每个实例池最大 20 个物理连接;
- 数据库允许的总连接数为 150;
- 其中需要为管理员、监控或其他服务保留 10 个连接。
应用理论上可能建立:
个连接,已经超过数据库可供业务使用的近似上限:
即使每个实例平时只使用 5 个连接,扩容、流量突增或连接预热也可能使总数达到 160。连接池配置应按所有实例的最坏总和检查,而不是只看单个实例。
在 PostgreSQL 中,max_connections 控制可接受的连接规模,并且还涉及为特定用途保留连接的配置。MySQL 中,max_connections 控制服务器允许的并发客户端连接数。具体可用连接数还会受到管理连接、复制、监控以及其他服务的影响,不能简单把配置值全部分给业务请求。
可以用以下 SQL 检查基础配置。
PostgreSQL:
SHOW max_connections;
SELECT count(*) AS current_connections
FROM pg_stat_activity;
示例输出可能是:
max_connections
-----------------
200
current_connections
---------------------
137
pg_stat_activity 中的行表示当前可见的服务器活动或会话记录。它不是“还可以安全建立多少个连接”的直接答案,因为并发创建、保留连接和权限边界仍然需要考虑。
MySQL:
SHOW VARIABLES LIKE 'max_connections';
SHOW STATUS LIKE 'Threads_connected';
SHOW STATUS LIKE 'Threads_running';
含义不同:
Threads_connected:当前连接数;Threads_running:当前处于运行状态的线程数,不等于连接数;max_connections:服务器连接上限。
Threads_connected 很高而 Threads_running 很低,可能表示大量空闲连接;Threads_running 持续接近连接上限,则更可能是查询、锁或 CPU 造成的并发压力。
2. 数据库连接容量与查询并发容量不是一回事
连接数为 100,不代表数据库能同时高效执行 100 条复杂查询。
设:
- :连接池允许的并发执行数;
- :一次数据库操作的平均服务时间;
- :每秒到达数据库的请求数。
在稳定状态下,排队论中的基本关系为:
其中:
- :系统中平均存在的请求数;
- :请求从进入到完成的平均时间;
- :吞吐率。
如果 100 个连接都执行平均 200 ms 的查询,那么在理想情况下单池吞吐上限近似为:
但这是非常理想化的估算。CPU、磁盘、锁、缓存命中率、网络和 WAL 等资源可能更早成为瓶颈。
更重要的是,增加连接数可能降低吞吐:
- 更多查询同时竞争 CPU;
- 更多事务持有锁;
- 更多工作内存被分配;
- 缓存局部性变差;
- 上下文切换增加;
- 大量查询同时访问磁盘,形成 I/O 队列。
因此,连接池大小应接近数据库在目标查询类型、目标延迟和目标并发下能够稳定处理的并发数,而不是盲目追随 CPU 核数或应用线程数。
3. 一个完整算例
假设:
- 4 个应用实例;
- 每个实例池上限为 12;
- 数据库连接总上限为 80;
- 需要保留 8 个管理和故障处理连接;
- 业务目标是最多允许 40 个并发数据库操作。
总池容量为:
数据库业务可用连接近似为:
所以从数据库连接上限角度看,48 没有超出 72。但业务目标只有 40 个并发数据库操作,48 个连接仍可能让过多查询同时进入数据库。
一种更保守的分配是每个实例最多 10 个:
但这只在实例数量稳定时成立。若自动扩容到 8 个实例,容量会变成:
又超过业务可用的 72。于是容量规划必须把扩容上限也纳入:
如果无法满足这个不等式,就必须使用集中式连接代理、降低每实例池上限、限制实例扩容,或改变数据库部署和分片方案。代理可以减少数据库看到的物理连接数,但不会自动消除后端查询并发和事务占用;事务级复用还要求客户端不能依赖跨请求的会话状态。
四、排队:连接池把压力放在哪里
当所有连接都在使用时,新请求有两种处理方式:
- 立即失败;
- 进入等待队列。
等待队列相当于把压力从数据库转移到应用进程。它可以吸收短暂尖峰,但不能消除超额负载。
1. 排队时间的组成
一个请求的总耗时可以粗略拆成:
其中:
- :等待连接池租约;
- :建立物理连接;
- :数据库内部等待 CPU、锁或 I/O;
- :真正执行 SQL 的时间;
- :传输和消费结果集的时间。
如果只记录“SQL 执行耗时”,可能看不到连接池等待;如果只记录请求总耗时,也无法知道慢在应用、连接池还是数据库。
2. 为什么排队会自我放大
假设连接池有 10 个连接,每条 SQL 平均执行 100 ms。理想吞吐约为:
如果请求到达率长期为 120 次/秒,系统的利用率为:
当 时,队列会不断增长,直到:
- 等待超时;
- 请求被上游取消;
- 应用线程或协程耗尽;
- 连接归还速度下降,进一步恶化。
这不是“池太小”这一句话就能解释的问题。池增大到 12,可能暂时使理论容量达到 120,但数据库也会承受更高的并发。正确做法是先确认瓶颈是连接数限制、查询服务时间、锁等待、CPU 还是 I/O。
3. 等待队列的工程语义
连接池等待应当有上限。无限等待会造成线程堆积,最终使连接池之外的 HTTP、RPC、消息消费线程也被占满。
一个请求从池中借连接时,至少应该定义:
- 最大池大小;
- 最小空闲连接数;
- 创建新连接的并发上限;
- 借连接超时;
- 等待队列是否有上限;
- 池关闭时如何唤醒等待者。
等待超时的含义是“本次请求没有及时获得连接”,不等于数据库查询失败。返回错误时,监控应区分:
pool_acquire_timeout
connect_timeout
db_lock_timeout
db_statement_timeout
network_error
否则运维人员可能错误地增加数据库连接数,而实际问题是某条 SQL 持有事务和锁太久。
五、超时:不同计时器解决不同问题
“设置超时”不是一个动作,而是一组作用对象不同的计时器。
1. 连接建立超时
连接建立超时覆盖 DNS、TCP、TLS、认证或协议握手中的一个或多个阶段,具体边界取决于客户端驱动。
它解决的是:
数据库地址不可达
网络丢包
服务端未接受连接
认证握手长期无响应
它不解决已经建立连接上的慢查询。
PostgreSQL 客户端通常可以通过连接参数设置 connect_timeout。MySQL 的 connect_timeout 主要与服务器等待客户端在连接握手阶段完成响应有关;客户端库也可能提供自己的连接超时参数。使用时必须查看所用驱动的语义,不能把同名参数当作跨产品、跨驱动的统一标准。
2. 连接池获取超时
这是应用等待空闲连接的时间:
请求 -> pool.acquire() -> 等待 -> 获得连接或失败
它不会取消已经在数据库执行的 SQL。如果池中 10 个连接都被慢查询占用,设置 200 ms 的获取超时只会让第 11 个请求快速失败,并不会让前 10 个查询停止。
3. SQL 执行超时
SQL 执行超时作用于数据库语句或客户端等待结果的过程。
PostgreSQL 的 statement_timeout 可以限制语句执行时间,超时时服务器取消当前语句;lock_timeout 用于等待锁的时间。lock_timeout 只限制锁等待,不限制 CPU 执行或磁盘读取。deadlock_timeout 不是“死锁最大允许时间”,而是服务器等待一段时间后检查死锁的配置,死锁检测和处理具有不同语义。
例如,PostgreSQL 可以在事务内临时设置:
BEGIN;
SET LOCAL statement_timeout = '2s';
SET LOCAL lock_timeout = '500ms';
UPDATE accounts
SET balance = balance - 100
WHERE id = 1;
COMMIT;
逐步解释:
BEGIN开启事务;SET LOCAL只在当前事务内生效,事务结束后恢复;- 如果等待锁超过 500 ms,锁请求失败;
- 如果语句总执行时间超过 2 s,语句被取消;
- 发生错误后,事务会进入失败状态,必须
ROLLBACK,不能直接继续执行后续 SQL; - 成功则提交。
如果 UPDATE 超时,正确恢复路径是:
ROLLBACK;
然后才能把连接归还连接池。若客户端在超时后无法确定服务端协议状态,销毁连接通常比复用更安全。
MySQL 的 InnoDB innodb_lock_wait_timeout 主要控制等待 InnoDB 行锁的时间。它不是所有 SQL 执行时间的通用上限,且锁等待、死锁、元数据锁等待和普通计算耗时可能由不同机制控制。MySQL 还支持针对部分语句类型的执行时间限制,但其作用范围和版本、语句类型有关,不能替代应用层总超时。
4. 事务空闲超时
“连接空闲”与“事务空闲”不是一回事:
连接空闲:
没有事务,等待下一条命令
事务空闲:
事务已经 BEGIN,但当前没有执行 SQL
第二种更危险,因为它可能保留快照和锁,阻止数据库回收旧版本。
PostgreSQL 提供 idle_in_transaction_session_timeout,用于终止长时间处于事务内空闲状态的会话。它能防止某些事务长期占用资源,但也会使应用收到连接断开或事务错误,因此应用必须正确处理。普通连接空闲超时和事务内空闲超时不能混为一谈。
MySQL 的 wait_timeout 主要针对非交互连接在空闲状态下的等待时间;它不是查询执行超时,也不能可靠替代应用层事务超时。服务器关闭空闲连接后,连接池中的对象可能变成陈旧连接,池必须在借出或使用失败时将其剔除并重建。
5. 客户端总超时与数据库超时的关系
设:
- :应用请求剩余时间;
- :连接池获取超时;
- :建立连接超时;
- :数据库语句超时;
- :结果读取超时。
合理关系通常是:
但这不是简单相加就能完全保证,因为各阶段可能重叠,驱动取消也可能需要额外时间。关键原则是:数据库内部超时不应晚于上游请求已经放弃的时间,否则数据库可能继续执行一个客户端已经不再等待的查询。
反例:
HTTP 超时:2 秒
数据库 statement_timeout:10 秒
2 秒后客户端断开,并不自动保证数据库立即停止执行。数据库可能继续运行剩余 8 秒,连接仍被占用,进而造成连接池耗尽。
六、事务、取消和连接复用的边界
1. 语句超时后为什么不能直接复用连接
以 PostgreSQL 为例,事务中的语句失败后,事务进入 aborted 状态:
BEGIN;
SELECT 1 / 0;
SELECT 2;
第二条语句不会正常执行,服务器会报告当前事务已失败,必须先:
ROLLBACK;
连接池若在第一条语句失败后直接归还连接,下一个请求执行任何 SQL 都可能遇到这个状态。
MySQL 的错误和事务状态规则与 PostgreSQL 不完全相同,但同样不能假设“任何 SQL 错误都不会改变连接可用性”。死锁回滚范围、锁等待超时行为、自动提交状态、未读结果等都应以具体引擎和驱动语义为准。
2. 客户端取消不是可靠的回滚证明
客户端超时后可能尝试发送取消请求,但存在以下情况:
- 取消请求没有到达服务器;
- 原查询已经完成,取消晚到;
- 网络中断导致客户端不知道服务器状态;
- 驱动取消后连接协议流中仍有未消费数据;
- 事务是否已提交变得不确定。
因此要区分两类失败:
明确未执行
例如在连接建立前失败,通常可以安全重试。
执行结果未知
例如请求已发送,随后网络断开。此时数据库可能:
- 尚未执行;
- 已执行但未提交;
- 已提交,但确认响应丢失。
对未知结果自动重试可能造成重复扣款、重复插入或重复发货。解决方法不是简单“重试三次”,而是使用幂等键、业务状态表、唯一约束或可查询的操作记录确认最终状态。
七、连接泄漏:不是只有忘记 close
1. 泄漏的定义
连接泄漏是指连接已经不再被业务使用,却没有回到池中,也没有被关闭。常见原因包括:
- 异常分支跳过归还;
- 超时取消后清理逻辑未执行;
- 流式结果集未关闭;
- 事务开启后请求被中途取消;
- 协程或线程持有连接跨越异步边界;
- 将连接放进全局对象或缓存;
- 连接池关闭时仍有借出连接;
- 归还连接时检测到协议状态异常,但没有销毁;
- 连接归还了,但事务或会话状态污染,表现为“逻辑泄漏”。
2. 如何区分泄漏和慢查询
假设连接池上限为 50:
active=50持续上升;- 数据库
Threads_running或 PostgreSQL 活动查询也持续增加; - 每个借用持续时间很长;
- 慢查询、锁等待明显。
这更像查询变慢或事务占用。
另一种情况:
active=50持续不降;- 数据库只有少量活动查询;
- 大多数连接在应用内部没有对应 SQL;
- 借用持续时间不断增加;
- 连接池等待超时增加。
这更像应用侧连接泄漏、忘记消费结果或忘记结束事务。
3. 泄漏检测应记录借用关系
连接池监控至少应记录:
pool_size
idle
active
pending_acquire
acquire_timeout_total
borrow_duration
connection_creation_failures
validation_failures
destroyed_connections
开发和排障阶段还可以记录:
- 借出连接的请求 ID;
- 借出时间;
- 调用栈;
- SQL 操作类别;
- 事务开始时间;
- 归还原因;
- 是否发生过超时或取消。
不要默认记录完整 SQL 和参数。参数可能包含密码、令牌、个人信息或业务机密,应进行脱敏,并遵循最小权限和审计要求。
一种简单的诊断思路是定义:
然后按分位数观察 borrow_duration。如果 P99 持续超过请求预算,连接池等待会快速积累。若某些调用栈的借用时间无限增长,则优先检查异常处理和结果集关闭。
八、数据库侧如何观察连接状态
1. PostgreSQL
查看会话状态:
SELECT
pid,
usename,
datname,
client_addr,
state,
wait_event_type,
wait_event,
xact_start,
query_start,
state_change,
now() - xact_start AS transaction_age,
now() - state_change AS state_age,
left(query, 200) AS query
FROM pg_stat_activity
WHERE datname = current_database()
ORDER BY xact_start NULLS LAST;
重点观察:
state = 'active':当前有查询活动;state = 'idle':连接空闲;state = 'idle in transaction':事务打开但当前没有查询;wait_event_type和wait_event:是否在锁、I/O 或其他等待;xact_start很早但state长期为空闲:可能存在长事务。
查找阻塞关系:
SELECT
blocked.pid AS blocked_pid,
blocked.query AS blocked_query,
blocker.pid AS blocker_pid,
blocker.query AS blocker_query
FROM pg_stat_activity AS blocked
JOIN pg_stat_activity AS blocker
ON blocker.pid = ANY(pg_blocking_pids(blocked.pid));
这类查询只能说明当前观察时刻的阻塞关系。处理阻塞前应确认业务影响和事务身份,不能看到一条长查询就直接终止。终止会话可能触发回滚,回滚本身也需要时间,并且可能影响正在进行的业务操作。
2. MySQL
查看连接和线程:
SHOW FULL PROCESSLIST;
SHOW STATUS LIKE 'Threads_connected';
SHOW STATUS LIKE 'Threads_running';
或者:
SELECT
ID,
USER,
HOST,
DB,
COMMAND,
TIME,
STATE,
INFO
FROM information_schema.PROCESSLIST
ORDER BY TIME DESC;
可以重点查看:
Sleep:空闲连接,不代表泄漏;Query:正在执行查询;Waiting for ... lock等状态:等待锁或其他资源;TIME:当前状态持续时间,不应单独当作 SQL 执行时间;INFO:当前 SQL,可能为空或被截断。
InnoDB 锁等待可以结合 performance_schema 的锁表和事务表分析。不同 MySQL 版本的监控字段和可见性可能有所变化,生产诊断应以目标版本的 performance_schema 文档为准。
九、连接失效与故障恢复
1. 连接为什么会变成陈旧连接
连接池中的连接可能在应用不知情时失效:
- 数据库重启;
- 主备切换;
- 防火墙清除空闲连接;
- NAT 或负载均衡器回收连接;
- 数据库主动终止会话;
- 网络分区;
- TLS 或认证配置变化;
- 服务端空闲连接超时。
TCP 连接在本地看起来仍然存在,不代表远端会话有效。真正使用连接时,应用可能收到连接重置、EOF、broken pipe 或协议错误。
2. 健康检查的取舍
池可以在借出前验证连接,例如执行轻量探测或使用驱动提供的 ping。它能减少把明显失效的连接交给业务,但有代价:
- 每次借出都增加网络往返;
- 检查成功到真正执行之间仍可能发生故障;
- ping 成功不证明事务、锁和会话状态正确;
- 大量连接同时健康检查可能形成额外压力。
因此健康检查不能替代执行时错误处理。实际通常结合:
- 空闲超过某时间后再验证;
- 建立连接时验证;
- SQL 执行遇到连接级错误时立即销毁;
- 连接回收前检查借用和事务状态。
3. 失效连接的处理路径
发生连接级错误时,池应把连接从可复用集合中移除:
业务执行 SQL
│
├─ 成功 ──> 提交/结束事务 ──> 归还空闲
│
├─ SQL 语义错误 ──> 按事务规则回滚 ──> 可能复用
│
├─ 锁等待超时 ──> 回滚/结束事务 ──> 按驱动状态决定复用
│
└─ 网络/协议错误 ──> 标记失效 ──> 关闭并重建
网络错误后不要执行“再发一次同样的写请求”作为通用恢复措施,因为结果可能已经提交。
4. 数据库重启后的重连风暴
数据库恢复后,所有应用实例可能同时发现连接失效,并同时执行:
关闭旧连接
-> 建立新连接
-> 认证
-> 预热
-> 重试业务请求
如果有 个实例、每个实例需要重建 条连接,则可能产生:
个近乎同时的连接请求。数据库刚恢复时还要执行恢复、回放日志、建立缓存,重连风暴会再次压垮它。
恢复策略应包含:
- 连接创建速率限制;
- 指数退避和随机抖动;
- 连接池预热上限;
- 熔断或暂时拒绝上游请求;
- 区分连接错误与业务错误;
- 限制重试次数;
- 只对幂等操作重试;
- 故障恢复后逐步放量。
退避时间可以采用:
其中:
- :第几次重试;
- :初始等待;
- :最大等待;
- :随机抖动范围。
随机抖动的目的,是避免所有实例按相同时间表再次连接。
5. 连接关闭与优雅停机
应用发布或缩容时,不能立即杀死所有进程而不处理连接。合理顺序是:
停止接收新请求
-> 等待或拒绝新的连接池借用
-> 让进行中的事务完成到截止时间
-> 超时事务回滚
-> 关闭池中的空闲连接
-> 关闭仍未完成的借用连接
如果应用使用消息队列,还要先暂停拉取或正确处理消息确认,否则数据库事务回滚后消息可能已被确认,形成业务不一致。
十、重试、幂等与事务边界
1. 哪些错误适合重试
通常更容易安全重试的是:
- 连接建立前失败;
- 明确的临时网络不可达,且业务操作尚未发送;
- 明确的锁等待超时,且事务已回滚;
- 数据库明确返回可重试的临时错误。
不应仅凭异常名称重试:
- 连接断开发生在写请求之后;
- 提交响应丢失;
- 事务状态未知;
- 违反唯一约束;
- 权限错误;
- SQL 语法错误;
- 数据校验错误。
2. 幂等写操作示例
一个业务请求可以携带唯一操作号 request_id,并由数据库约束保证去重:
CREATE TABLE payment_request (
request_id varchar(64) PRIMARY KEY,
account_id bigint NOT NULL,
amount decimal(18, 2) NOT NULL,
created_at timestamp NOT NULL
);
处理逻辑:
BEGIN;
INSERT INTO payment_request(request_id, account_id, amount, created_at)
VALUES ('req-20250101-0001', 42, 100.00, CURRENT_TIMESTAMP)
ON CONFLICT (request_id) DO NOTHING;
-- 根据插入结果决定是否执行后续一次性业务变更
COMMIT;
这是 PostgreSQL 语法。MySQL 需要使用相应的唯一键和冲突处理语法,不能直接照搬 ON CONFLICT。
该模式的核心不是“重试一定安全”,而是把重试转换为同一个业务操作的再次确认。唯一约束、事务和状态记录共同提供了可验证的幂等边界。
十一、容量、查询优化和安全边界是连在一起的
1. 慢查询会伪装成连接池容量不足
如果一条查询原本执行 50 ms,后来因为基数估计错误选择了低效 Join 算法,执行时间变成 2 s,那么相同连接数下每个连接能完成的请求数会显著下降:
连接池等待增加并不一定说明池太小,可能是:
- 统计信息过期;
- 估算行数与实际行数差异巨大;
- Join 顺序错误;
- Nested Loop 在大结果集上退化;
- 缺少适合过滤条件的索引;
- 锁等待或磁盘 I/O 增加。
因此连接池指标应和查询计划、数据库等待事件、CPU、I/O、锁等待一起分析。单独增加池大小,可能只是把慢查询数量增加。
2. 安全配置也会影响连接和池
连接池复用的是带有身份和会话状态的数据库连接,因此安全问题可能跨请求传播:
- 连接使用高权限账号;
- 租户上下文通过会话变量设置,却没有归还前清理;
- 日志打印包含密码或敏感参数;
- 未启用 TLS,连接认证和数据可能暴露;
- 连接串包含明文凭据;
- 审计记录无法关联请求 ID;
- 动态 SQL 拼接造成注入。
应使用最小权限账号、参数化查询、加密传输和必要的审计。连接池并不能降低 SQL 注入风险;反而因为连接长期复用,会让错误的会话状态持续更久。
十二、常见误解与对应的失败表现
误解一:连接池越大,吞吐越高
失败表现:数据库 CPU、锁等待、上下文切换上升,P99 延迟变差。
原因:连接池只是允许更多并发进入数据库,不能增加 CPU、内存、磁盘或锁资源。
误解二:空闲连接很多就是泄漏
失败表现:看到数据库有大量 idle 或 MySQL Sleep 连接,立即杀连接。
原因:空闲连接可能是连接池的正常缓存。应查看池的 active、pending、借用时长和数据库事务状态。真正危险的是 idle in transaction 或长期持有事务的连接。
误解三:客户端请求超时后,数据库查询一定停止
失败表现:HTTP 请求已经失败,但数据库活动查询和连接占用仍持续增加。
原因:应用层断开、驱动取消和数据库取消是不同动作,网络故障时还可能无法确认取消结果。
误解四:数据库重启后重试几次就够了
失败表现:数据库恢复瞬间出现大量认证、连接和查询请求,随后再次过载。
原因:所有实例同时重连和重试,形成同步风暴;并且写操作结果可能处于未知状态。
误解五:回滚后任何连接都能复用
失败表现:协议错误、未读结果、会话变量污染或驱动状态异常在后续请求中出现。
原因:回滚只解决一部分事务状态问题,不能证明连接协议流和会话状态已经健康。
十三、建立可验证的故障排查路径
遇到“获取数据库连接超时”时,可以按下面顺序缩小范围:
第一步:看应用池状态
确认:
总连接数
空闲连接数
使用中连接数
等待借连接数
借连接超时数
连接创建失败数
借用时长分布
active接近上限,pending增加:先看慢 SQL、锁和事务;active不高但获取仍失败:检查池实现、连接状态计数或线程/协程调度;- 创建失败多:检查数据库上限、网络、认证和 TLS;
- 借用时长无限增长:检查泄漏和未结束事务。
第二步:看数据库连接与活动
PostgreSQL 看 pg_stat_activity 的状态、事务年龄和等待事件;MySQL 看 PROCESSLIST、Threads_connected、Threads_running 以及锁等待信息。
第三步:区分等待位置
记录时间线:
请求进入
-> 开始等待连接
-> 获得连接
-> 开始发送 SQL
-> 收到首字节
-> 结果读取完成
-> 事务提交
-> 归还连接
只有这样才能区分:
- 应用排队;
- 建连慢;
- 数据库锁等待;
- SQL 执行慢;
- 结果集读取慢;
- 提交或网络响应慢;
- 归还逻辑未执行。
第四步:确认是否发生扩容或故障
检查:
- 应用实例数是否增加;
- 每实例池是否重新初始化;
- 数据库是否刚重启或主备切换;
- 网络设备是否回收空闲连接;
- 是否发生批量重试;
- 是否有单个租户或接口产生异常流量。
第五步:恢复时先保护数据库
在原因尚未确认时,不要直接把所有池上限调大。更安全的临时动作可能是:
- 限制入口并发;
- 暂停高成本任务;
- 减少重试;
- 清理明确的长事务;
- 隔离异常实例;
- 限制重连速率;
- 恢复后逐步放量。
终止数据库会话、调整连接上限和修改超时都应先确认版本语义、权限影响和回滚方式。
十四、一个可操作的配置推导框架
可以从业务预算反推连接池,而不是从“线程数乘以某个倍数”开始。
已知:
- 目标数据库请求吞吐为 ;
- 目标平均数据库服务时间为 ;
- 允许的并发余量系数为 ,例如用于吸收波动;
- 应用实例最大数为 。
所需并发连接的初步估算为:
每个实例池上限约为:
然后检查:
这只是容量起点,不是性能保证。验证时还要使用实际查询分布和压力测试观察:
- 数据库 CPU;
- I/O 延迟;
- 锁等待;
- 事务年龄;
- 查询 P95/P99;
- 连接池等待 P95/P99;
- 错误率和超时率;
- 扩容后的总连接数。
如果服务时间 因慢查询从 100 ms 增加到 500 ms,连接需求和排队风险都会变化。此时优先修复执行计划、索引、事务范围或锁竞争,通常比继续增加连接池更合理。
连接池的本质不是“多开一些数据库连接”,而是把有限的数据库会话资源交给应用统一调度。容量决定最多允许多少工作进入数据库,排队决定超额工作暂时堆在哪里,超时决定系统何时放弃,事务生命周期决定连接能否安全复用,泄漏决定资源是否永久脱离调度,故障恢复则决定一次数据库中断会不会演变成全系统重试风暴。
只有把这些状态和边界分别测量、分别处理,连接池才真正承担起隔离突发流量和控制并发的作用。
系列导航与关联阅读
- 系列入口:数据库完整学习路线:从关系模型、事务索引到分布式与向量检索
- 上一篇:MVCC、锁与死锁:可见性、锁粒度、等待图和线上诊断
- 下一篇:数据库迁移与在线变更:Expand-Contract、锁风险和可回滚发布
- 延伸:SQL 执行计划与查询优化:基数估计、Join 算法和慢查询诊断
- 延伸:数据库安全治理:最小权限、加密、审计、脱敏与注入防护
官方资料
本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。

评论
0 条讨论