连接池归还连接时,DISCARD 到底该跑哪条命令
连接池把连接还给池子时,最常见的做法是跑一条“重置”命令。问题是 PostgreSQL、Redis、MySQL 对“重置”的定义各不相同:有的只清事务队列,有的把整个会话打回启动状态,有的连序列缓存和计划缓存一起丢掉。用错了,轻则性能抖动,重则 currval() 报错、连接状态残留。
下面分别拆理三家的命令语义边界和代价,最后给一张对照表和上线前检查清单。
PostgreSQL:DISCARD 的五个变体
PostgreSQL 的 DISCARD 用于释放与一个数据库会话相关的内部资源,用来部分或完全重置会话状态。它的语法如下(DISCARD 是 PostgreSQL 扩展,不是标准 SQL):
DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP }
各变体的行为:
PLANS:释放所有已缓存的查询计划,下次使用相关预备语句时重新做计划。SEQUENCES:丢弃所有已缓存的序列相关状态,包括currval()/lastval()信息,以及任何还没被nextval()返回的预分配序列值。TEMPORARY/TEMP:删除当前会话中创建的所有临时表。ALL:包含上面所有形式,并重置额外状态。
DISCARD ALL 展开后等价于:
CLOSE ALL;
SET SESSION AUTHORIZATION DEFAULT;
RESET ALL;
DEALLOCATE ALL;
UNLISTEN *;
SELECT pg_advisory_unlock_all();
DISCARD PLANS;
DISCARD TEMP;
DISCARD SEQUENCES;
来源:https://www.postgresql.org/docs/current/sql-discard.html ,中文文档:http://postgres.cn/docs/current/sql-discard.html
陷阱一:RESET ALL 回的是会话启动值
RESET ALL 把参数恢复到会话启动时的值。这个“启动值”来自 postgresql.conf、ALTER ROLE / ALTER DATABASE 设置,以及启动包或连接串传进来的参数,编码默认值并不在这个范围内。
后果:连接串里带 options='-c statement_timeout=30s',或者给角色配了 ALTER ROLE app SET search_path = app, public,跑完 RESET ALL 之后这些设置仍然留着。清理逻辑不能假设执行完就回到初始状态。
陷阱二:DISCARD ALL 不能在事务块内
在事务里执行会直接报错,SQLSTATE 25001:
ERROR: DISCARD ALL cannot run inside a transaction block
连接池做归还清理时,要确认当前连接不在事务中(包括隐式开启的事务)。很多池的清理逻辑是先回滚再跑 DISCARD,顺序错了就会撞上这个错误。
陷阱三:DISCARD SEQUENCES 会造成序列空洞
序列预分配值(CREATE SEQUENCE 的 CACHE 参数)是进程内缓存的。DISCARD SEQUENCES 把这些还没被 nextval() 返回的值直接丢掉,结果是:
- 序列出现空洞,丢多少取决于 cache 大小和丢弃时机;
- 同一会话里之前拿到的
currval()失效,后续再调用会报错,需要重新nextval()。
对依赖 lastval() 的逻辑,或者对连续性有要求的流程,这条命令有实际影响。
陷阱四:DISCARD PLANS 的代价
清空计划缓存意味着所有预备语句下一次执行都要重新 parse/plan。在高频归还的连接池里反复跑 DISCARD ALL,等于把计划缓存变成一次性缓存,短查询的解析开销会稳定地叠加上去。
Redis:DISCARD 只丢事务队列,RESET 才是全量
Redis 的 DISCARD 和 PostgreSQL 同名,但语义窄得多:
DISCARD
- since 2.0.0,group=transactions,复杂度 O(N),N 为已排队的命令数;
- 丢弃
MULTI中所有之前排队的命令,恢复连接状态; - 如果之前用过
WATCH,DISCARD会 unwatch 该连接监视的所有键; - 返回 OK。
它只做两件事:丢 MULTI 队列、取消 WATCH。不重置 SELECT 的库、不重置 CLIENT REPLY、不退出订阅、不改协议版本、不要去认证。
来源:https://redis.io/docs/latest/commands/discard/
Redis 6.2 加了 RESET:
RESET
官方描述是对连接的服务端上下文做一次完整重置,效果等同于断开重连。对普通客户端连接依次做:
- 丢弃当前
MULTI事务块; UNWATCH所有被WATCH的键;- 关闭
CLIENT TRACKING; - 把连接设为
READWRITE; - 取消
ASKING模式; CLIENT REPLY设为ON;- 协议版本设为
RESP2; SELECT 0;- 退出
MONITOR模式; - 退出
SUBSCRIBE/PSUBSCRIBE订阅状态; - 去认证(需要重新
AUTH); - 关闭
NO-EVICT; - 关闭
NO-TOUCH。
返回 RESET,复杂度 O(1)。来源:https://redis.io/docs/latest/commands/reset/
兼容性上有一个限制:Redis Software / Redis Cloud 的兼容表里 RESET 标为不支持(Standard 和 Active-Active 都不支持),自建 Redis 可用。连接池做清理逻辑时要按部署形态分支处理。
MySQL:COM_RESET_CONNECTION
MySQL 协议层的重置是 COM_RESET_CONNECTION,C API 对应:
int mysql_reset_connection(MYSQL *mysql);
它重置连接以清除会话状态,效果类似 mysql_change_user() 或自动重连,但连接不关闭重开、不重新认证,write set session history 也会被重置。
对连接相关状态的影响:
- 回滚任何活动事务,并重置 autocommit 模式;
- 释放所有表锁;
- 关闭并删除所有
TEMPORARY表; - 会话系统变量重新初始化为对应全局系统变量的值,包括
SET NAMES等语句隐式设置的变量; - 丢失用户自定义变量(user variables);
- 释放 prepared statements;
- 关闭
HANDLER变量; LAST_INSERT_ID()重置为 0;- 释放
GET_LOCK()获得的锁; - 清除通过
mysql_bind_param()设置的当前 query attributes。
来源:https://dev.mysql.com/doc/c-api/8.4/en/mysql-reset-connection.html
关键点在于,会话系统变量回到的是全局系统变量的当前值,并非连接建立时的值。如果 DBA 在连接生命周期内改过 GLOBAL 变量,重置后的值会和刚连上时不一样。另外 user variables、GET_LOCK 锁、临时表、prepared statement 全部丢失,依赖这些做跨语句状态的业务逻辑,在连接池里本来就不该假设它们存在。
PgBouncer:清理命令跑在哪个池化模式
PgBouncer 的 server_reset_query 默认是 DISCARD ALL,用于 session pooling 归还连接时清理。
在 transaction / statement pooling 模式下,这条查询默认不执行,因为事务池里连接不归还给应用,每个事务结束就跑一次 DISCARD ALL 会把计划缓存和临时对象反复清掉。需要强制执行得设 server_reset_query_always = 1,代价同上。
所以选型问题在于你的池化模式允不允许跑它;跑与不跑的好坏并不是重点。
三家对照
| 维度 | PostgreSQL DISCARD ALL | Redis DISCARD | Redis RESET | MySQL COM_RESET_CONNECTION |
|---|---|---|---|---|
| 事务 | 不能在事务块内(25001) | 丢弃 MULTI 队列 | 丢弃 MULTI 队列 | 回滚活动事务、重置 autocommit |
| 监控 / 订阅 | UNLISTEN * | 不动订阅 | 退出 MONITOR / SUBSCRIBE | 无对应 |
| 认证 | 不涉及 | 不涉及 | 去认证,需重新 AUTH | 不重新认证 |
| 库选择 | 不改 | 不改 SELECT 的库 | SELECT 0 | 不涉及 |
| 会话变量 | RESET ALL 回会话启动值 | 无 | 设为 READWRITE 等固定值 | 回全局变量当前值 |
| 锁 | pg_advisory_unlock_all() | 无 | 无 | 释放表锁和 GET_LOCK |
| 临时对象 | DISCARD TEMP | 无 | 无 | 关闭并删除 TEMPORARY 表 |
| 预编译 / 计划 | DEALLOCATE ALL + DISCARD PLANS | 无 | 无 | 释放 prepared statements |
| 序列缓存 | DISCARD SEQUENCES,有空洞风险 | 无 | 无 | 无对应 |
| 用户变量 | 无 | 无 | 无 | 丢失 |
上线前检查清单
- 确认池化模式:session pooling 才默认跑
server_reset_query;transaction pooling 要显式开server_reset_query_always,并评估计划缓存被反复清空的代价。 - 确认归还连接时不在事务里,避免
DISCARD ALL撞 25001。 - 有序列依赖的服务,评估
DISCARD SEQUENCES带来的currval失效和空洞是否可接受,否则从重置脚本里去掉这一项。 - 连接串或角色级设置的参数在
RESET ALL之后仍然生效,清理逻辑不要假设能回到初始状态。 - Redis 部署在 Redis Software / Redis Cloud 上的,不要依赖
RESET;用DISCARD加显式清理SELECT、AUTH、订阅状态。 - 用
DISCARD做清理的,要补上库选择、回复模式、订阅状态的重置,DISCARD本身不处理这些。 - MySQL 侧确认业务没有依赖 user variables、
GET_LOCK、临时表、prepared statement 跨语句存活。 - MySQL 全局变量可能在运行期被改,会话重置后的值不等于建连时的值。