概述
当 PostgreSQL 的连接执行一批 SQL的时候,SQL里面有DML 也有查询。如果整批 SQL 是一个事务,其中有 SQL 执行报错时,事务的状态会进入 “ABORTED”状态,此后无论执行什么 SQL(正确的、或者查询SQL),都会报错:
ERROR: current transaction is aborted, commands ignored until end of transaction block
复制
此时需要用命令 COMMIT 或 ROLLBACK 清理会话的事务状态。这点跟其他数据库行为不一致。
上层应用需要捕获事务异常并及时发起回滚命令。
事务报错现象及分析
PostgreSQL 测试版本 14.4 。
pg_test=# show server_version;
server_version
--------------------------------
14.4 (Debian 14.4-1.pgdg110+1)
(1 row)
复制
表不存在报错
先看一个简单的报错场景(表不存在)。
pg_test=# \echo :AUTOCOMMIT
on
pg_test=# \set AUTOCOMMIT off
pg_test=*# select datname, pid, application_name , backend_start , xact_start ,query_start ,state , backend_xid
from pg_stat_activity psa where 1=1
and pid=pg_backend_pid()
;
datname | pid | application_name | backend_start | xact_start | query_start | state | backend_xid
---------+-----+------------------+-------------------------------+------------------------------+-------------------------------+--------+-------------
pg_test | 264 | psql | 2023-03-23 13:09:23.419058+08 | 2023-03-23 13:09:33.35136+08 | 2023-03-23 13:09:33.351609+08 | active |
(1 row)
pg_test=*# select * from t_test;
ERROR: relation "t_test" does not exist
LINE 1: select * from t_test;
^
pg_test=!# select * from t_test;
ERROR: current transaction is aborted, commands ignored until end of transaction block
pg_test=!#
复制
案例说明
会话关闭了AUTOCOMMIT,查询一个不存在的表,第一次报错表不存在,第二次就报事务已经中断。如果只看第二次报错现象,不一定能看出为什么报错。
select datname, pid, application_name , backend_start , xact_start ,query_start ,state , backend_xid
from pg_stat_activity psa where 1=1
and pid=247
;
复制
注意:上面这个 SQL 用于查看当前会话对应 PG 上的进程 PID 以及相关信息(重点是事务信息)。奇怪的是如果查看当前会话时,字段 backend_xid 值总是空的,得另开一个独立窗口(并且那个窗口会话必须开启事务自动提交)查询才有结果。怀疑这是 PostgreSQL 的 BUG。
PG 事务报错特点
先说结论。会话禁用了自动提交,所以是显式事务。
Postgres数据库中,一个会话开启事务后如果某个 SQL 中出错的话,那这个会话以后的 SQL 都会出错。此时事务已经回滚了,只是会话的事务状态需要清理。调用 commit 或者 rollback 都可以清理会话事务状态。
详细分析验证一下上面结论。
首先看上面报错的图,当关闭自动提交后,发起查询,通过字段 xact_start 可以看出会话开启了事务。然后再通过窗口2再查一下这个会话信息。
pg_test=# select datname, pid, application_name , backend_start , xact_start ,query_start ,state , backend_xid
from pg_stat_activity psa where 1=1
and pid=264
;
datname | pid | application_name | backend_start | xact_start | query_start | state | backend_xid
---------+-----+------------------+-------------------------------+------------------------------+-------------------------------+---------------------+-------------
pg_test | 264 | psql | 2023-03-23 13:09:23.419058+08 | 2023-03-23 13:09:33.35136+08 | 2023-03-23 13:09:33.351609+08 | idle in transaction |
(1 row)
复制
通过字段 state 可以看出前面那个会话处于“idle in transaction”。这个叫空闲事务。由于字段 backend_xid 为空,并没有真正修改什么,有的地方也叫“只读事务”。
在第一次查询不存在的表 t_test 报错之后,在窗口 2中再次查询窗口1的会话状态,变化如下。
pg_test=# select datname, pid, application_name , backend_start , xact_start ,query_start ,state , backend_xid
from pg_stat_activity psa where 1=1
and pid=264
;
datname | pid | application_name | backend_start | xact_start | query_start | state | backend_xid
---------+-----+------------------+-------------------------------+------------+-------------------------------+-------------------------------+-------------
pg_test | 264 | psql | 2023-03-23 13:09:23.419058+08 | | 2023-03-23 13:10:01.055564+08 | idle in transaction (aborted) |
(1 row)
复制
字段 xact_start 值被清空,表示后端 PG 上的事务已经释放(通过回滚释放,后面示例可以验证这点)。不过会话的状态字段 state 的值变为 “idle in transaction (aborted)”。再次强调,这表示会话的状态,或者叫会话的事务状态,不是事务的状态。
此时窗口1会话继续查询不存在的表 t_test 就会变成新的报错信息。此时,除了 ROLLBACK 或 COMMIT 外 其他 SQL 都是会报错。
会话曾经的事务已经报错回滚了,会话的事务状态需要清理。PG 的这个设计让用惯 ORACLE 的人很不习惯,有点反人性。
下面解决一下这个报错。
窗口1 会话发起 rollback 命令。窗口2 查看窗口1 会话的状态,如下。
pg_test=# select datname, pid, application_name , backend_start , xact_start ,query_start ,state , backend_xid
from pg_stat_activity psa where 1=1
and pid=264
;
datname | pid | application_name | backend_start | xact_start | query_start | state | backend_xid
---------+-----+------------------+-------------------------------+------------+-------------------------------+-------+-------------
pg_test | 264 | psql | 2023-03-23 13:09:23.419058+08 | | 2023-03-23 13:16:13.788448+08 | idle |
(1 row)
复制
此时窗口的会话1的字段 state 已经变为 “idle”,字段 “query_start" 更新为 ROLLBACK 发起的时间,只是字段 xact_start 还没有值,表示还没有新的事务。
此时在窗口1会话再发起查询就都正常了。
下面继续再看一个唯一性约束报错来验证上面这个。
表唯一性约束报错
pg_test=# select datname, pid, application_name , backend_start , xact_start ,query_start ,state , backend_xid
from pg_stat_activity psa where 1=1
and pid=pg_backend_pid()
;
datname | pid | application_name | backend_start | xact_start | query_start | state | backend_xid
---------+-----+------------------+-------------------------------+-------------------------------+-------------------------------+--------+-------------
pg_test | 264 | psql | 2023-03-23 13:09:23.419058+08 | 2023-03-23 13:16:53.961122+08 | 2023-03-23 13:16:53.961234+08 | active |
(1 row)
pg_test=*# create table t_test(id BIGSERIAL not null , c1 varchar(50) not null unique , gmt_create timestamp not null default current_timestamp, gmt_modified timestamp not null default current_timestamp );
CREATE TABLE
pg_test=*# insert into t_test(c1) values('ONE');
INSERT 0 1
pg_test=*# insert into t_test(c1) values('ONE');
ERROR: duplicate key value violates unique constraint "t_test_c1_key"
DETAIL: Key (c1)=(ONE) already exists.
pg_test=!# insert into t_test(c1) values('TWO');
ERROR: current transaction is aborted, commands ignored until end of transaction block
pg_test=!#
pg_test=!# COMMIT;
ROLLBACK
pg_test=# select * from t_test;
ERROR: relation "t_test" does not exist
LINE 1: select * from t_test;
复制
复制
案例说明
创建一个有唯一约束列的表,插入重复的记录报唯一性约束冲突,重新插入新值报事务中断报错。然后用 COMMIT 清理会话事务状态,再次查询表,报表不存在错误。
原因分析
创建表 t_test 后,通过窗口2查看窗口1会话状态,如下。
pg_test=# select datname, pid, application_name , backend_start , xact_start ,query_start ,state , backend_xid
from pg_stat_activity psa where 1=1
and pid=264
;
datname | pid | application_name | backend_start | xact_start | query_start | state | backend_xid
---------+-----+------------------+-------------------------------+-------------------------------+-------------------------------+---------------------+-------------
pg_test | 264 | psql | 2023-03-23 13:09:23.419058+08 | 2023-03-23 13:21:47.053503+08 | 2023-03-23 13:22:01.987016+08 | idle in transaction | 23499
(1 row)
复制
字段 backend_xid 有值表示窗口1的会话开启了真正的事务,并且 create table 语句产生了事务还没有提交。这也是 PostgreSQL 跟 ORACLE 不一样的地方,DDL 并不会隐式提交事务。这个特性对应用迭代发布挺友好的。
当插入重复的记录报唯一约束错误后,通过窗口2 查看窗口1 会话状态,字段 state 变为“idle in transaction (aborted)”,字段 xact_start 和 backend_xid 都清空了,说明会话1 的事务已经回滚掉了,只是会话的事务状态还需要清理。
pg_test=# select datname, pid, application_name , backend_start , xact_start ,query_start ,state , backend_xid
from pg_stat_activity psa where 1=1
and pid=264
;
datname | pid | application_name | backend_start | xact_start | query_start | state | backend_xid
---------+-----+------------------+-------------------------------+------------+-------------------------------+-------------------------------+-------------
pg_test | 264 | psql | 2023-03-23 13:09:23.419058+08 | | 2023-03-23 13:22:28.338452+08 | idle in transaction (aborted) |
(1 row)
复制
当窗口1会话发起 COMMIT 命令时,客户端返回的是 “ROLLBACK",这也是提示此前的事务是回滚掉了,会话状态被清理掉了。通过窗口2查看。
pg_test=# select datname, pid, application_name , backend_start , xact_start ,query_start ,state , backend_xid
from pg_stat_activity psa where 1=1
and pid=264
;
datname | pid | application_name | backend_start | xact_start | query_start | state | backend_xid
---------+-----+------------------+-------------------------------+------------+-------------------------------+-------+-------------
pg_test | 264 | psql | 2023-03-23 13:09:23.419058+08 | | 2023-03-23 13:22:51.545165+08 | idle |
(1 row)
复制
这里的 COMMIT 并不会提交之前的修改。所以再次查看表时,依然报不存在。此时再次通过窗口2查看会话1的状态,进入新的一轮报错。字段 query_start 时间跟上面比发生了变化。
pg_test=# select datname, pid, application_name , backend_start , xact_start ,query_start ,state , backend_xid
from pg_stat_activity psa where 1=1
and pid=264
;
datname | pid | application_name | backend_start | xact_start | query_start | state | backend_xid
---------+-----+------------------+-------------------------------+------------+-------------------------------+-------------------------------+-------------
pg_test | 264 | psql | 2023-03-23 13:09:23.419058+08 | | 2023-03-23 13:22:59.996803+08 | idle in transaction (aborted) |
(1 row)
复制
JAVA 应用会话事务报错现象分析1
表不存在报错
这里用客户端 DBeaver 来模拟 Java 应用。DBeaver 连接 PostgreSQL 使用的是 JDBC 驱动。
案例说明
DBeaver 的连接默认 AUTOCOMMIT 是打开的,所以这里通过 begin 显式开启事务。这一条非常重要,对后面的结果有影响。
连着发起 3条 SQL,DBeaver 应该是逐条执行的。根据前面的案例,表 t_test 不存在,第一次访问会报错表不存在,第二次访问会报事务中断。
现象分析
从上图可以看出,实际报错停留在第一次访问表的时候,DBeaver 并没有继续执行下面一条 SQL。作为一款客户端工具, DBeaver 不会主动去改变会话的事务状态(COMMIT或ROLLBACK),留待用户处理。
假设此时再单独执行第三条 SQL,则报错事务已经中断,跟此前案例分析一致。
表唯一性约束报错
回滚掉上面会话事务,然后继续第二案例测试。
报错也是 停留在插入重复数据的地方。
字段传值类型不一致报错
回滚掉上面会话事务,然后继续测试一个传值类型不对报错。
begin;
create table t_test(id BIGSERIAL not null , c1 varchar(50) not null unique , gmt_create timestamp not null default current_timestamp, gmt_modified timestamp not null default current_timestamp );
insert into t_test(c1) values('ONE');
select * from t_test;
update t_test set gmt_modified=current_timestamp, c1='ONE-ONE' where c1=1;
select * from t_test;
复制
报错现象如下,停留在第5条SQL处。
此时如果单独执行最后一句 SQL,也会报错事务已中断。
这个现象跟预期的一致。
JAVA应用会话事务报错现象分析2
跟前面现象分析1的区别在于这次我将 JDBC 驱动连接的 AutoCommit设置为 false,即关闭自动提交。
PG 连接属性关闭自动提交
PostgreSQL JDBC 驱动官方文档地址:https://jdbc.postgresql.org/documentation/use/ 。其中有个属性是我们关注的。
autosave
autosave (String) Default never
Specifies what the driver should do if a query fails. In autosave=always mode, JDBC driver sets a savepoint before each query, and rolls back to that savepoint in case of failure. In autosave=never mode (default), no savepoint dance is made ever. In autosave=conservative mode, savepoint is set for each query, however the rollback is done only for rare cases like ‘cached statement cannot change return type’ or ‘statement XXX is not valid’ so JDBC driver rolls back and retries
复制
这个控制的是当执行一批 SQL 时,是否在每个SQL 执行前都做一次事务快照(savepoint),然后再SQL报错的时候能回滚到上一个事务快照处。默认是不需要这么做。
这里我们也不动这个。可以看到 DBeaver 也没有改变这个。
在 DBeaver 的数据库连接里关闭自动提交。
这个影响是这个数据源的所有连接。另外一个方法是针对每个窗口可以在工具栏里单独关闭自动提交。这里测试建议用后者(因为查询会话状态时为了确保数据准确,用自动提交模式)。
当关闭会话的事务改为手动提交的时候,再运行 begin 命令不会报错,但是有个提示:
there is already a transaction in progress
复制
复制
字段传值类型不一致报错
再次运行下面案例。
select datname, pid, application_name , backend_start , xact_start ,query_start ,state , backend_xid
from pg_stat_activity psa where 1=1
and pid=pg_backend_pid()
;
create table t_test(id BIGSERIAL not null , c1 varchar(50) not null unique , gmt_create timestamp not null default current_timestamp, gmt_modified timestamp not null default current_timestamp );
insert into t_test(c1) values('ONE');
select * from t_test;
update t_test set gmt_modified=current_timestamp, c1='ONE-ONE' where c1=1;
select * from t_test;
复制
运行结果:
在问题SQL处报错,最后一条SQL没有执行。这个跟前面测试结果也是一致的。
此时新开一个独立的窗口查看该会话的状态。
会话字段 xact_start 和 backend_xid 都是空的,会话状态是“idle in transaction (aborted)”。表示这个会话的事务已经回滚了,只是会话状态还是 事务中断状态。
此时再继续运行最后一个 SQL,结果跟前面测试不一样。
此时不再报说事务已中断,而是报表不存在。表不存在是因为事务回滚了,表自然也没有了。多次测试发现,此时如果换别的正常SQL,是能出结果的。可见此时不需要用 COMMIT或ROLLBACK 就可以跑SQL 。
初步推测 PG的JDBC 驱动连接在关闭事务自动提交后(setAutoCommit(false))后,对于失败的事务的会话状态可能会在下一个SQL执行前自动清理了。
PG 事务保存点(savepoint)
PG 虽然不支持事务嵌套,但是支持事务保存点技术。
在 dbeaver 里开启事务自动提交,使用 begin/commit 手动控制事务。运行下面 SQL。
begin;
create table t_test(id BIGSERIAL not null , c1 varchar(50) not null unique , gmt_create timestamp not null default current_timestamp, gmt_modified timestamp not null default current_timestamp );
insert into t_test(c1) values('ONE');
select * from t_test;
savepoint insert_one ;
insert into t_test(c1) values('TWO');
select * from t_test;
insert into t_test(c1) values('ONE');
select * from t_test;
复制
案例说明
创建一个有约束的表,先后插入多笔记录,中间使用事务保存点。最后插入的值是重复值。
现象如下
这个报错跟前面测试是一致的。查看会话事务状态,事务信息还在。
此时单独执行最后一个 SQL ,提示事务已中断。这一步也符合预期。
紧接着我们回滚到上一个保存点。
rollback to insert_one ;
复制
再次独立窗口查看会话1的状态,字段 state 已变为正常值“idle in transaction”。
最后查看表记录发现第一笔插入还在。
以上是手动控制回滚到某个事务保存点。
我们再把 会话事务改为手动提交模式看看。
回滚整个事务重新测试。
报错地点和报错提示符合预期。
独立窗口查看会话1的状态,事务信息也还在。
此时如果直接查看表,就会报错提示表不存在,然后会话的事务信息也彻底没有了。
但是手动回滚到事务保存点后,会话的事务状态也是正确的。
此后查看表的记录第一笔插入的记录还在,跟前面结果一致。
由此可见,当JDBC 连接关闭了事务自动提交并开启了事务保存点之后,PG 是提供了让用户自己控制回滚点的机会。如果用户不用,那么驱动就会自动回滚整个事务并清理会话的事务状态。
总结
PG 的会话里事务出现语句报错,默认自动回滚整个事务并需要用户显式的清理会话的事务状态后才允许继续执行其他SQL。PG的 JDBC 驱动在关闭事务自动提交后会在会话事务里有语句报错后 在用户执行下一个 SQL前自动清理会话的事务状态。这个行为特征跟 ORACLE不完全一样。
类似的案例在 MySQL 里跑时,MySQL 提供了参数让用户选择事务中语句失败的时候是只回滚失败语句的变更,还是回滚整个事务的变更。这个更人性化一些。
类似的案例在 OceanBase 里也有,只不过不是语句执行失败,而是 OceanBase 的事务有个超时机制,一旦事务没有及时提交导致超时后,也是需要用户主动发起 COMMIT/ROLLBACK 命令去清理会话的事务状态(超时状态)。否则,后面的所有 SQL 不管正确与否都会报错(报事务已超时)。