一直以来都对数据库的事务一知半解,虽然听说过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在首次查询之后将无法更改事务级别。
场景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