目 录CONTENT

文章目录

【GaussDB】1个会话1次SQL请求1秒钟狂涨四万个事务号

DarkAthena
2026-10-13 / 0 评论 / 0 点赞 / 3 阅读 / 0 字

【GaussDB】1个会话1次SQL请求1秒钟狂涨四万个事务号

背景

众所周知,原生PG的32位xid设计是个令用户非常诟病的一个问题,跑到42亿个事务必须得处理事务号回卷。曾经有客户反馈过,他们在原生PG上跑一个业务系统,2天就要冻结回卷,维护起来很是麻烦。而GaussDB/openGauss的64位xid则让事务号的上限增加到了1800亿亿‌,是42亿个事务的42亿倍,可以说不用再担心xid回卷的问题。

虽然GaussDB已经是64位xid了,但是这并不是简单地把32位xid类型全部改成64位xid类型,它的实现中仍然存在32位xid,具体原理本文不展开,想了解的请自行查阅文档或用AI分析openGauss源码。

上上个月,某客户生产环境的GaussDB监控开始告警,一套刚上线半个月的GaussDB环境,事务号已经来到了二十亿。令人费解的是,这套数据库上的应用系统,是内部使用的,负载并不大,活动会话也不多,不是那种高频交易的系统,事务号不应该涨得那么快。

由于华为之前有过预警,部分GaussDB内核版本,在事务号超过42亿的时候,特定的一些场景下会触发内核BUG,现象是执行SQL时索引扫描报错(预警编号:CLD-P-A-202582,GaussDB的预警不公开,细节请联系华为客服)。而客户使用的版本正好落在了存在此BUG的版本范围内。所以客户特意加了事务号的监控告警。本来想着一套业务量极少的系统应该不会触发这个告警,可没想到这个告警来得这么快。

分析

该应用系统不会通过java去执行dml语句,都是调用的存储过程,而它所有的存储过程中,都没有在循环语句中使用commit/rollback,提交和回滚都是在循环外面的。

检查了dbe_perf.statement,也并未发现可疑的SQL。

所以,那种循环内执行dml+commit导致事务号激增的可能性不大。

其实分析过原生PG xid暴涨场景的,可能还会怀疑一个东西,即savepoint。原生PG中,savepoint是通过子事务来实现的,不过这套应用系统里既没有显式的savepoint语句,也没有在jdbc上配置autosave=always。

提到子事务,又不得不提plpgsql里的 exception ,在原生PG中,exception的块级回滚也是靠子事务来实现的,不过这套应用系统也不存在大量循环里再套含exception的plsql块的情况。

对于ORACLE兼容场景,要求事务中出错可以语句级回滚,原生PG的方案大概率就会选择在jdbc里开autosave,但这对存储过程没用,因为JDBC里的autosave本质上是客户端自动发送了savepoint语句,但调用存储过程的时候,存储过程内的每个语句间无法自动插进去savepoint。

于是乎,以PG为底子的二开数据库,往往会新增一个兼容性参数,让数据库服务端自动去加savepoint。GaussDB/openGauss便是用的这种方式:

behavior_compat_options = 'plstmt_implicit_savepoint'
控制存储过程中更新语句的执行是否拥有独立的子事务。

若设置此配置项,存储过程中每条更新语句前开启隐式保存点,EXCEPTION块中默认回退到最近的保存点,从而保证只回退失败语句的修改。该选项是为了兼容Oracle数据库的EXCEPTION行为。

这个开关,在506~507版本,由于a_format_dev_version默认值是s7,而s7里就带了 behavior_compat_options='display_leading_zero, end_month_calculate, plstmt_implicit_savepoint, truncate_numeric_tail_zero, char_coerce_compat, rownum_type_compat, aformat_null_test, aformat_regexp_match, disable_rewrite_nesttable, compat_cursor, allow_procedure_compile_check, plsql_security_definer, proc_outparam_override, proc_implicit_for_loop_variable, dynamic_sql_compat, proc_outparam_transfer_length, varray_compat, tableof_elem_constraints, current_sysdate, plsql_rollback_keep_user, enable_bpcharlikebpchar_compare, show_full_error_lineno, enable_crosstype_integer_operator, forbid_package_function_with_prefix, enable_case_when_alias, time_constexpr_compact, enable_use_ora_timestamptz, forbid_skip_tableof_empty_str_elem, sys_function_without_brackets', 因此本质上默认是开了plstmt_implicit_savepoint的。

写程序时,逻辑里带上异常处理,是良好的开发习惯,往往开发规范里也会要求一定要带exception。该客户的存储过程是全部都带有exception的,因此极有可能是因为此因素导致。

模拟测试

创建了一张表,使用带exception的匿名块循环插入40000行,匿名块里面不提交,匿名块结束后自动提交,在循环开始前后结束后,从pg_get_xidlimit函数(这里用txid_current不对)里拿到下一个事务号,以此计算出事务号的增长,整个匿名块是作为一个sql语句发送给服务端的:

gaussdb=# set a_format_dev_version to s6;
SET
gaussdb=# set behavior_compat_options ='plstmt_implicit_savepoint';
SET
gaussdb=# create table t_1(a int);
CREATE TABLE
gaussdb=# declare
gaussdb-# x1 int;
gaussdb-# x2 int;
gaussdb-# y1 timestamp;
gaussdb-# begin
gaussdb$#     select next_xid::text::int into x1 from pg_get_xidlimit();
gaussdb$#     y1:=clock_timestamp();
gaussdb$#     for i in 1..40000 loop
gaussdb$#         insert into t_1 values (1);
gaussdb$#     end loop;
gaussdb$#     select next_xid::text::int into x2 from pg_get_xidlimit();
gaussdb$#     raise notice 'start xid:%,end xid:%,diff:%',x1,x2,x2-x1;
gaussdb$#     raise notice 'time :% ',clock_timestamp()-y1;
gaussdb$#     exception when others then
gaussdb$#     raise notice '%',sqlerrm;
gaussdb$# end;
gaussdb$# /
NOTICE:  start xid:5023487,end xid:5063489,diff:40002
NOTICE:  time :00:00:01.061652 
ANONYMOUS BLOCK EXECUTE
gaussdb=# 

注意,我这个测试环境是没有任何其他业务负载的,如果有其他负载,理论上这个diff会比我测出来的结果要更大。

可以看到循环结束后的下一个事务号和循环开始的下一个事务号之间相差40002,这和循环内部执行的insert语句次数是接近的。而这也仅仅只花了1.06秒钟,我这还是普通PC,如果机器性能更好,再多一些并发,可以预见这个事务号消耗会有多快。

可能有些对数字不敏感的人感受不到,这里我大概估算一下,假设某个系统只有2个连接,使用上面这种方式24小时不停地写入,那么消耗的事务个数为
24*60*60*40000*2=6,912,000,000 ,69亿!尽管24小时不停是极端情况,而且写入的字段多时,每秒执行次数肯定会下降。但要注意,我这里只按2个连接数来算,真正的业务系统,从几十到几千个连接都是非常常见的。不要忘了原生PG的事务号只有42亿!

当然,重点并不在于1秒钟会跑出多少个事务号,而是,客户的存储过程代码中,明明没有显式的commit/rollback,仅仅是个循环的DML,就让事务号飞速消耗了,几乎是执行一个dml语句就是一个事务!

为了确认的确是否是plstmt_implicit_savepoint这个功能导致,我们可以再做两个测试:

一、去掉exception

gaussdb=# declare
gaussdb-# x1 int;
gaussdb-# x2 int;
gaussdb-# y1 timestamp;
gaussdb-# begin
gaussdb$#     select next_xid::text::int into x1 from pg_get_xidlimit();
gaussdb$#     y1:=clock_timestamp();
gaussdb$#     for i in 1..40000 loop
gaussdb$#         insert into t_1 values (1);
gaussdb$#     end loop;
gaussdb$#     select next_xid::text::int into x2 from pg_get_xidlimit();
gaussdb$#     raise notice 'start xid:%,end xid:%,diff:%',x1,x2,x2-x1;
gaussdb$#     raise notice 'time :% ',clock_timestamp()-y1;
gaussdb$#     --exception when others then
gaussdb$#     raise notice '%',sqlerrm;
gaussdb$# end;
gaussdb$# /
NOTICE:  start xid:5144733,end xid:5144734,diff:1
NOTICE:  time :00:00:00.575441 
NOTICE:  <NULL>
ANONYMOUS BLOCK EXECUTE
gaussdb=# 

可以看到同样还是40000次插入,但事务号增长只有1了,同时这个时间开销也降到了0.575秒,几乎只有exception时的一半,也就是说使用子事务必然会导致性能的严重损耗。

二、保留exception,关闭plstmt_implicit_savepoint功能

gaussdb=# set a_format_dev_version to s6;
SET
gaussdb=# set behavior_compat_options ='';
SET
gaussdb=# declare
gaussdb-# x1 int;
gaussdb-# x2 int;
gaussdb-# y1 timestamp;
gaussdb-# begin
gaussdb$#     select next_xid::text::int into x1 from pg_get_xidlimit();
gaussdb$#     y1:=clock_timestamp();
gaussdb$#     for i in 1..40000 loop
gaussdb$#         insert into t_1 values (1);
gaussdb$#     end loop;
gaussdb$#     select next_xid::text::int into x2 from pg_get_xidlimit();
gaussdb$#     raise notice 'start xid:%,end xid:%,diff:%',x1,x2,x2-x1;
gaussdb$#     raise notice 'time :% ',clock_timestamp()-y1;
gaussdb$#     exception when others then
gaussdb$#     raise notice '%',sqlerrm;
gaussdb$# end;
gaussdb$# /
NOTICE:  start xid:5185087,end xid:5185089,diff:2
NOTICE:  time :00:00:00.585661 
ANONYMOUS BLOCK EXECUTE
gaussdb=# 

可以看到这里事务号增长加了2,和前面一个测试相比,也就是exception增加了1个子事务。

结论

根据以上测试,可以论证plstmt_implicit_savepoint这个功能在有循环dml且带exception的存储过程里,是会极大加速事务号的消耗的,并且还会带来比较大的性能下降。但好在GaussDB已经是64位xid,影响也就只有性能了(不考虑bug的情况下)。

生产环境验证

找到一个有循环dml次数多的存储过程业务,先查一下当前事务号txid_current(),然后跑一下业务,再查一下当前事务号txid_current(),发现数字暴涨了。根据该业务执行频率,大概可得该业务半个月能涨多少事务号,类似方法估算其他业务,合计起来符合该环境整体的事务号增长速度。

建议

如果应用系统不依赖语句级回滚这个特性,所有的逻辑写的都是只要报错就把未提交的回滚,那么完全可以关闭掉plstmt_implicit_savepoint这个功能,这样不仅事务号增长会放缓,而且性能也能得到提高。

如果依赖plstmt_implicit_savepoint这个功能,那么建议升级数据库或者打相关修复补丁。

延伸讨论

我从部分渠道了解到,其他有些基于PG的国产数据库为了模拟ORACLE这种语句级回滚的能力,都是选择了autosave的方案,但有些并没有支持到64位xid,可想在其上的高频交易系统会面临怎样的窘境。

以下是我用deepseek搜集到的源自PG且使用了64位xid的数据库,可能不全:

数据库与 PG 的关系64位事务号机制引入版本时间关键信息
Postgres Pro Enterprise商业分支64 位 XID9.6 / 132018年1月 / 2021年6月9.6 版本文档已列出“64 位事务 ID 不受回卷影响”;13 版本将其作为核心企业特性正式对外发布。
openGauss华为开源分支64 位 XID2.0.12019年openGauss 将 XID 从 32bit 改为 64bit,官方对比文章明确提及此变化;openGauss 于 2019 年 9 月首次开源,2.0.1 是当时已发布的最新版本。
GaussDB华为商业版(openGauss 同源)64 位 XID未明确未明确,但肯定早于openGauss华为官方文档确认 GaussDB 内部 XID 为 64 位,彻底解决回卷问题,但未公开具体引入版本。
PolarDB for PostgreSQL阿里云 PG 分支64 位 CSN2.0(兼容 Oracle 版)2022 年 11 月2.0 版本引入 CSN 事务快照机制,使用单调递增的 64 位整数替代原生 xid 进行 MVCC 可见性判断。
KingbaseES电科金仓 PG 分支64 位 MXID → 64 位 XIDV9 → V009R002C0162026年8月28日V9 引入 64 位 MXID 解决 MXID 回卷;V009R002C016 版本将 XID 全面扩展至 64 位,避免 32 位 XID 回收不及时导致的业务中断。

当然,解决事务id只有42亿的问题,并不只有64位xid这一种方案;而且事务号强制冻结回卷这种机制也不一定非要沿用;语句级回滚的支持也并非必须走savepoint。只是各家都倾向于使用市面上已经较为成熟的公开方案。这里我不评价优劣,但原生PG至今(2026)仍然遗留了42亿xid回卷的"特性",可以说是相当遗憾了。

0
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin
博主关闭了所有页面的评论