事务控制(BEGIN/COMMIT/ROLLBACK/SAVEPOINT)
本教程共 50 篇 · 第 46 篇 · 更新于 2026-07-31 · 约 7 分钟阅读
46. 事务控制(BEGIN/COMMIT/ROLLBACK/SAVEPOINT)
本节目标:理解事务的 ACID 特性,掌握显式事务、回滚与保存点的写法,以及出错时如何处理。
什么是事务
事务(TRANSACTION)把多条 SQL 打包成一个”要么全成、要么全不成”的整体。比如转账:A 扣钱、B 加钱,两步必须一起成功,不能只成一半。
PostgreSQL 默认每条语句自己就是一个事务(自动提交)。想多句绑一起,就用手动事务。
ACID 四特性
- 原子性(Atomicity):全做或全不做。
- 一致性(Consistency):数据始终满足约束。
- 隔离性(Isolation):并发事务互不干扰。
- 持久性(Durability):提交后数据永久保存。
自动提交
你平时在 psql 里敲一条 UPDATE,回车就生效,背后其实是 PostgreSQL 自动帮你包了一层 BEGIN ... COMMIT。这叫”自动提交”(autocommit)。
在 psql 里可以用 \set AUTOCOMMIT off 关掉自动提交,之后每条语句都得自己 COMMIT 才生效。调试时很有用,但别忘了提交,否则改动只对当前会话可见、连接断开就丢了。
显式事务:BEGIN / COMMIT
用 BEGIN 开头, COMMIT 收尾:
postgres=# BEGIN;
postgres=# UPDATE accounts SET balance = balance - 100 WHERE id = 1;
postgres=# UPDATE accounts SET balance = balance + 100 WHERE id = 2;
postgres=# COMMIT;
只有 COMMIT 之后,改动才真正生效并对外可见。COMMIT 之前,别的会话看不到你的修改。
回滚:ROLLBACK
中途发现不对,用 ROLLBACK 全部撤销,回到事务开始前的样子:
postgres=# BEGIN;
postgres=# DELETE FROM orders WHERE id = 999;
postgres=# ROLLBACK; -- 删除被撤销,数据还在
Tip没写 BEGIN 的单条 SQL,PostgreSQL 会自动包一层 BEGIN … COMMIT,这叫”自动提交”。所以平时你改了数据不显式提交也生效,背后是它在干活。
保存点:SAVEPOINT
保存点(SAVEPOINT)让你在事务里设”checkpoint”,只回退到某一点,前面的改动保留。
postgres=# BEGIN;
postgres=# UPDATE accounts SET balance = balance - 100 WHERE id = 1;
postgres=# SAVEPOINT sp1;
postgres=# UPDATE accounts SET balance = balance + 100 WHERE id = 2;
postgres=# -- 发现该加给 3 号,回退到 sp1
postgres=# ROLLBACK TO sp1;
postgres=# UPDATE accounts SET balance = balance + 100 WHERE id = 3;
postgres=# COMMIT;
ROLLBACK TO sp1 之后,sp1 还在,可以反复回退到它。用不到了可以 RELEASE SAVEPOINT sp1 释放。
Note保存点可以嵌套:sp1 里再建 sp2,
ROLLBACK TO sp1会把 sp2 一起撤掉。深层保存点常用来保护”这一段操作”,失败时只退这一段。
出错时整个事务中止
这是新手最容易踩的坑:事务里只要有一条语句报错,整个事务就进入”中止状态”,后面所有语句都会报 current transaction is aborted。
postgres=# BEGIN;
postgres=# UPDATE accounts SET balance = balance - 100 WHERE id = 1;
postgres=# UPDATE accounts SET balance = balance / 0 WHERE id = 2; -- 除零报错
postgres=# UPDATE accounts SET balance = balance + 100 WHERE id = 3; -- 也报错:事务已中止
postgres=# ROLLBACK; -- 只能用 ROLLBACK 整体撤掉
Warning报错导致事务进入中止状态时,只有
ROLLBACK TO 保存点或整体ROLLBACK能救回来,不能接着往下写普通语句。所以长事务里要善用 SAVEPOINT,出错就ROLLBACK TO到最近的保存点继续。
设置隔离级别与只读
事务开始后可以设定隔离级别和读写模式:
postgres=# BEGIN;
postgres=# SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
postgres=# SET TRANSACTION READ ONLY;
postgres=# SELECT * FROM accounts; -- 只读事务,不能改数据
postgres=# COMMIT;
隔离级别有 READ UNCOMMITTED(PG 里等同于 READ COMMITTED)、READ COMMITTED(默认)、REPEATABLE READ、SERIALIZABLE。具体差异见第 47 章。
保存点的细节:回退后还在吗
ROLLBACK TO sp1 只撤回到 sp1 那个点,sp1 本身依然有效,你可以再往后改、再回退,反复横跳。只有 RELEASE SAVEPOINT sp1 才会把它彻底释放,之后不能再 ROLLBACK TO sp1。
嵌套保存点也很常见:在 sp1 里再建 sp2,回退到 sp1 会连 sp2 一起消失。这其实就是”子事务”的实现机制。
长事务的风险
事务开得越久,风险越大:
- 占着的锁一直不释放,别的会话容易被卡住。
- 期间产生的旧版本行不能被
VACUUM清理,表会膨胀。 - 连接意外断开,整个事务回滚,前面白干。
Warning别在事务里做”等用户确认”之类的人机交互。曾经有人开事务后去吃午饭,回来发现整库写入都堵住了。长事务要短平快。
事务内临时改参数:SET LOCAL
事务里想临时改个参数(比如只在这段事务里放宽某个限制),用 SET LOCAL。事务一提交/回滚,参数自动还原:
postgres=# BEGIN;
postgres=# SET LOCAL work_mem = '64MB'; -- 只对本事务有效
postgres=# -- 跑一段吃内存的查询
postgres=# COMMIT; -- 之后 work_mem 恢复默认值
只读事务的用途
设成只读的事务,能防止误改数据,也方便做一致的报表快照:
postgres=# BEGIN;
postgres=# SET TRANSACTION READ ONLY;
postgres=# SELECT * FROM orders; -- 可以查
postgres=# UPDATE orders SET amount = 0; -- 报错:只读事务
postgres=# COMMIT;
应用里的事务
你在代码里用的 ORM / 驱动,本质也是这套:BEGIN 对应”开事务”,commit() 对应 COMMIT,抛异常时框架帮你 ROLLBACK。理解这一章,就能看懂代码里”事务没提交导致数据没存”之类的坑。
事务隔离级别预告
本章只讲了怎么开关事务。PostgreSQL 还有四种隔离级别(READ COMMITTED 默认、REPEATABLE READ、SERIALIZABLE 等),它们决定并发时你能看到别的会话的哪些改动。这块内容多、坑也多,下一章(第 47 章)会专门展开。这里先有个概念:事务不只是”提交或回滚”,还关系到”并发下能看到什么”。
面试常问:事务到底解决了什么
如果被人问”为什么需要事务”,记住 ACID 四个字就够展开:原子性保证要么全成要么全不成(转账不会钱丢了),一致性保证不破坏约束,隔离性保证并发互不干扰,持久性保证提交后不丢。日常写代码,把”必须一起成功”的多步操作包进 BEGIN ... COMMIT,就是事务最朴素也最有用的用法。
常见误区
- 以为
ROLLBACK TO之后事务还能像没出错一样继续写。可以,但前提是没进入”中止状态”;一旦中止只能ROLLBACK TO 保存点或整体ROLLBACK。 - 忘了
COMMIT,在 psql 里关掉自动提交后改动一直没保存。 - 把多步操作不包事务,结果中途失败,前面的改动了、后面的没动,数据不一致。
- 在事务块里 CALL 一个带内部 COMMIT 的存储过程,直接报错(见第 45 章)。
- 误以为 ROLLBACK 能回退已经 COMMIT 的事务。COMMIT 之后就永久了,撤不回。