编程 DISCARD 不是万能重置:PostgreSQL、Redis、MySQL 清理会话状态的边界与代价

2026-10-23 00:03:47

连接池归还连接时,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 ALLRedis DISCARDRedis RESETMySQL 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 全局变量可能在运行期被改,会话重置后的值不等于建连时的值。
复制全文 生成海报 DISCARD PostgreSQL Redis MySQL 连接池 会话状态

推荐文章

程序员茄子在线接单