小透明
发布于 2025-06-20 / 41 阅读
1
0

PostgreSQL事务错误处理解析

一直以来都对数据库的事务一知半解,虽然听说过MySQL和PostgreSQL在报错时的处理上的不同,但是从未真正查阅资料或动手验证过。今天将以PostgreSQL为例,实际测试它在不同场景下的处理逻辑。

本文内容较为基础,适合新手阅读。

环境信息

数据库:PostgreSQL 17.5 (Debian 17.5-1.pgdg120+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 12.2.0-14) 12.2.0, 64-bit

客户端:psql (PostgreSQL) 17.4

测试用的表:

create table t1 (
  id bigserial primary key,
  name text not null,
  created_at timestamp
);

场景1:开启事务后,是否可以更改事务级别

begin;
-- 在客户端2插入一条数据,但不提交
-- 设置隔离级别为读已提交
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 查询是否存在数据
select * from t1;
-- 设置隔离级别为读未提交
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- 查询是否存在数据
select * from t1;

结果:

postgres=# begin;
BEGIN
postgres=*# SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET
postgres=*# select * from t1;
 id | name | created_at
----+------+------------
(0 行记录)


postgres=*# SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
ERROR:  SET TRANSACTION ISOLATION LEVEL must be called before any query
postgres=!# commit;
ROLLBACK
postgres=#

结论

SET TRANSACTION必须在任意查询或数据修改语句之前调用,否则会报错。PostgreSQL在首次查询之后将无法更改事务级别。

相关文档:PostgreSQL:文档:17:设置事务

场景2:如果两个事务同时更新一条数据会发生什么

先预先插入一条数据

insert into t1(name, created_at) values('test', now()) returning id;

分别开启两个客户端,并执行以下SQL

begin;
-- 客户端1
update t1 set name = 'test1' where id = 1;
-- 客户端2
update t1 set name = 'test2' where id = 1;
-- 不提交事务,观察客户端的结果
end;

结果:

-- 客户端1
postgres=# begin;
BEGIN
postgres=*# update t1 set name = 'test1' where id = 1;
UPDATE 1
postgres=*#

-- 客户端2
postgres=# begin;
BEGIN
postgres=*# update t1 set name = 'test1' where id = 1;
-- 这里在等待客户端1事务结束

结论

当两个事务同时更新一条数据时,先更新的事务会直接更新成功,第二个事务会在执行更新语句后被阻塞,直到事务1结束。

这部分内容涉及到数据库锁的相关知识,这里就不继续深入了,只关注当没有获取到锁时是如何处理的。

select for update也可以达到相同的效果。

场景3:当事务中发生错误时,是否可以继续提交

违反非空约束

postgres=# begin;
BEGIN
postgres=*# insert into t1(name, created_at) values('s3', now());
INSERT 0 1
postgres=*# insert into t1(name, created_at) values(null, now());
ERROR:  null value in column "name" of relation "t1" violates not-null constraint
描述:  Failing row contains (7, null, 2025-06-20 11:07:36.176331).
postgres=!# insert into t1(name, created_at) values('s3', now());
ERROR:  current transaction is aborted, commands ignored until end of transaction block
postgres=!# commit;
ROLLBACK
postgres=#

违反唯一性约束

postgres=# begin;
BEGIN
postgres=*# insert into t1(name, created_at) values('s3', now());
INSERT 0 1
postgres=*# insert into t1(id, name, created_at) values(1, 's3', now());
ERROR:  duplicate key value violates unique constraint "t1_pkey"
描述:  Key (id)=(1) already exists.
postgres=!# insert into t1(name, created_at) values('s3', now());
ERROR:  current transaction is aborted, commands ignored until end of transaction block
postgres=!# commit;
ROLLBACK
postgres=#

其他错误

-- 除0错误
postgres=# begin;
BEGIN
postgres=*# insert into t1(name, created_at) values('s3', now());
INSERT 0 1
postgres=*# select 1/0;
ERROR:  division by zero
postgres=!# insert into t1(name, created_at) values('s3', now());
ERROR:  current transaction is aborted, commands ignored until end of transaction block
postgres=!# commit;
ROLLBACK
postgres=#

-- 类型转换错误
postgres=# begin;
BEGIN
postgres=*# insert into t1(name, created_at) values('s3', now());
INSERT 0 1
postgres=*# SELECT CAST('invalid' AS INT);
ERROR:  invalid input syntax for type integer: "invalid"
第1行SELECT CAST('invalid' AS INT);
                 ^
postgres=!# insert into t1(name, created_at) values('s3', now());
ERROR:  current transaction is aborted, commands ignored until end of transaction block
postgres=!# commit;
ROLLBACK
postgres=#

-- 主动抛出异常
postgres=# begin;
BEGIN
postgres=*# insert into t1(name, created_at) values('s3', now());
INSERT 0 1
postgres=*# DO $$
postgres$*# BEGIN
postgres$*#     RAISE EXCEPTION '主动抛出的异常'
postgres$*#         USING DETAIL = '异常详情',
postgres$*#               HINT = '错误提示',
postgres$*#               ERRCODE = 22000;
postgres$*# END $$;
ERROR:  主动抛出的异常
描述:  异常详情
提示:  错误提示
背景:  PL/pgSQL function inline_code_block line 3 at RAISE
postgres=!# insert into t1(name, created_at) values('s3', now());
ERROR:  current transaction is aborted, commands ignored until end of transaction block
postgres=!# commit;
ROLLBACK
postgres=#

结论

当事务中出现任意错误时,该事务只能回滚,无法再提交。

场景4:当发生错误时,回滚到指定savepoint后,是否可以继续提交

postgres=# begin;
BEGIN
postgres=*# insert into t1(id, name, created_at) values(1, 's4', now());
INSERT 0 1
postgres=*# savepoint p1;
SAVEPOINT
postgres=*# insert into t1(id, name, created_at) values(1, 's4_1', now());
ERROR:  duplicate key value violates unique constraint "t1_pkey"
DETAIL:  Key (id)=(1) already exists.
postgres=!# rollback to p1;
ROLLBACK
postgres=*# commit;
COMMIT
postgres=# select * from t1 where id = 1;
 id | name |         created_at
----+------+----------------------------
  1 | s4   | 2025-06-29 19:52:55.312435
(1 row)

结论

当事物中发生错误后,如果回滚到发生错误之前的savepoint后,可以消除事物的错误状态,事物此时可以继续执行或提交。

场景5:嵌套执行存储过程时,当内部发生错误时,是如何处理事务的

用到的存储过程

create or replace procedure test1()
LANGUAGE plpgsql
AS $$
BEGIN
    insert into t1(name, created_at) values('s5_1', now());
	begin 
		CALL test2();
	exception
		when others then
		RAISE NOTICE '捕获了一个错误';
		insert into t1(name, created_at) values('s5_3', now());	
	end;
	
END;
$$;

create or replace procedure test2()
LANGUAGE plpgsql
AS $$
BEGIN
    insert into t1(name, created_at) values('s5_2', now());
	RAISE EXCEPTION 'Error s5';
END;
$$;

执行过程

postgres=# select * from t1;
 id | name | created_at
----+------+------------
(0 rows)


postgres=# DO $$
postgres$# BEGIN
postgres$#   CALL test1();
postgres$# END;
postgres$# $$;
NOTICE:  捕获了一个错误
DO
postgres=# select * from t1;
 id | name |         created_at
----+------+----------------------------
 14 | s5_1 | 2025-06-29 20:38:49.333041
 16 | s5_3 | 2025-06-29 20:38:49.333041
(2 rows)

结论

当存储过程抛出异常,如果异常被捕获,则对应begin块中的修改会被回滚,外层的改动不变。如果异常没有被捕获,则整个事物都得被回滚。

相关文档:PostgreSQL: Documentation: 17: 41.6. Control Structures

场景6:DDL会否会提前提交事物

TODO


评论