PostgreSQL TRANSACTION(事务)(手把手讲解)

什么是 PostgreSQL TRANSACTION(事务)?

在数据库的世界里,事务就像一次“银行转账”操作。你从 A 账户转 100 元到 B 账户,这个操作必须同时满足两个条件:A 账户扣款成功,B 账户加款成功。如果其中任何一个步骤失败,整个操作就得撤销,不能只完成一半。

在 PostgreSQL 中,PostgreSQL TRANSACTION(事务) 就是用来保证这类操作“要么全部成功,要么全部失败”的机制。它确保数据库在并发访问和异常情况下依然保持数据的一致性。

举个例子:你在网上买一本书,系统要完成以下几步:

  1. 扣除你的账户余额;
  2. 增加商家的收款金额;
  3. 更新库存数量。

如果第 2 步失败,比如网络中断,那第 1 步的扣款就不该生效。否则你钱没了,书也没到,岂不是亏大了?

这就是事务的核心价值:原子性(Atomicity)。事务中的所有操作要么全部执行,要么一个都不执行。

事务的四大特性(ACID)

PostgreSQL TRANSACTION(事务) 的强大之处,源于它严格遵循 ACID 原则。这四个字母代表了事务的四大基本特性:

  • A:原子性(Atomicity)
    事务是一个不可分割的最小工作单元。要么全部完成,要么全部回滚。

  • C:一致性(Consistency)
    事务执行前后,数据库必须保持一致状态。比如余额总和不能凭空消失或增加。

  • I:隔离性(Isolation)
    多个事务并发执行时,彼此之间互不干扰。即使同时操作同一张表,也不会出现“脏读”或“幻读”。

  • D:持久性(Durability)
    一旦事务提交,其结果就会永久保存在数据库中,即使系统崩溃也不会丢失。

这就像一场精心编排的舞台剧:每个演员(操作)都必须准确完成自己的台词(动作),不能半途而废;剧终后,所有情节都必须被完整记录下来。

实际案例演示

我们来创建一个简单的账户表,模拟转账操作:

-- 创建账户表
CREATE TABLE accounts (
    id SERIAL PRIMARY KEY,
    name VARCHAR(50) NOT NULL,
    balance DECIMAL(10,2) DEFAULT 0.00
);

插入初始数据:

-- 插入两个账户
INSERT INTO accounts (name, balance) VALUES ('Alice', 1000.00);
INSERT INTO accounts (name, balance) VALUES ('Bob', 500.00);

现在我们开始一个事务,实现从 Alice 转 200 元给 Bob。

-- 开始事务
BEGIN;

-- 从 Alice 账户扣款
UPDATE accounts SET balance = balance - 200.00 WHERE name = 'Alice';

-- 向 Bob 账户加款
UPDATE accounts SET balance = balance + 200.00 WHERE name = 'Bob';

-- 提交事务,所有操作生效
COMMIT;

执行后,Alice 剩下 800,Bob 变成 700,一切正常。

但如果中途出错,比如 Bob 的账户名拼错:

BEGIN;

UPDATE accounts SET balance = balance - 200.00 WHERE name = 'Alice';
UPDATE accounts SET balance = balance + 200.00 WHERE name = 'Bod'; -- 错误:拼写错误

-- 这里没有提交,而是回滚
ROLLBACK;

此时,Alice 的钱没被扣,Bob 的账户也没加钱。事务被完整撤销,数据库状态未改变。

事务的控制语句详解

PostgreSQL 提供了几个关键的命令来控制事务流程:

  • BEGINSTART TRANSACTION:开启一个新事务。
  • COMMIT:提交事务,永久保存更改。
  • ROLLBACK:回滚事务,撤销所有未提交的操作。
  • SAVEPOINT:设置保存点,允许部分回滚。

保存点(Savepoint):事务中的“撤销点”

想象你在写一篇论文,写到一半发现前面某段写错了,但不想重头再来。你可以先“保存”当前进度,等修改完再决定是否回退到这个点。

在 PostgreSQL 中,SAVEPOINT 就是这个功能。

BEGIN;

UPDATE accounts SET balance = balance - 100.00 WHERE name = 'Alice';
SAVEPOINT sp1; -- 设置保存点

UPDATE accounts SET balance = balance + 50.00 WHERE name = 'Bob';
SAVEPOINT sp2; -- 再设一个保存点

-- 发现第二个操作有问题,只想回滚到 sp1
ROLLBACK TO SAVEPOINT sp1;

-- 此时 Alice 减了 100,Bob 没变
COMMIT;

这个例子中,我们通过 SAVEPOINT 实现了局部回滚,避免了整个事务被放弃。

事务隔离级别与并发控制

在高并发场景下,多个事务可能同时操作同一张表。这时就需要隔离级别来控制事务之间的可见性。

PostgreSQL 支持以下几种隔离级别(从低到高):

隔离级别 描述
READ UNCOMMITTED 读取未提交的数据,可能出现脏读,PostgreSQL 不支持此级别
READ COMMITTED 默认级别,只能读取已提交的数据,避免脏读
REPEATABLE READ 保证同一事务内多次读取相同数据结果一致
SERIALIZABLE 最高隔离级别,完全避免并发问题,但性能最低

PostgreSQL 默认使用 READ COMMITTED,这意味着每个语句看到的都是最新已提交的数据。

实际测试:脏读与不可重复读

我们用两个终端(或两个 psql 会话)来模拟并发:

会话 A:

BEGIN;
UPDATE accounts SET balance = balance - 100.00 WHERE name = 'Alice';
-- 不提交,保持事务打开

会话 B:

-- 在会话 B 中查询 Alice 的余额
SELECT balance FROM accounts WHERE name = 'Alice';
-- 结果会显示 900(因为 A 未提交)

此时会话 B 读到了未提交的数据——这就是“脏读”。但在 PostgreSQL 中,如果你用 READ COMMITTED,它会自动避免这种情况。

如果使用 REPEATABLE READ,同一事务内多次查询结果一致,即使其他事务已提交修改。

事务的最佳实践建议

1. 尽量缩短事务时间

长事务会占用锁资源,影响其他用户的操作。尽量将事务控制在几秒内完成。

-- ❌ 不推荐:长时间操作在事务中
BEGIN;
-- 处理大量数据,耗时 30 秒
UPDATE big_table SET status = 'processed';
-- 期间无法访问该表
COMMIT;
-- ✅ 推荐:拆分事务
BEGIN;
UPDATE big_table SET status = 'processed' WHERE id < 10000;
COMMIT;

BEGIN;
UPDATE big_table SET status = 'processed' WHERE id >= 10000 AND id < 20000;
COMMIT;

2. 使用 SAVEPOINT 处理复杂逻辑

当一个事务包含多个步骤,其中某一步可能失败时,使用保存点可以避免全盘回滚。

3. 避免在事务中执行耗时操作

如文件 I/O、网络请求、复杂的计算等,这些操作不应放在事务内,否则可能阻塞数据库。

4. 明确使用 COMMITROLLBACK

不要让事务“悬而未决”。程序结束前,必须显式提交或回滚。

-- 错误示例:忘记提交
BEGIN;
UPDATE accounts SET balance = balance + 100 WHERE name = 'Alice';
-- 程序结束,事务未提交,资源被锁定

总结

PostgreSQL TRANSACTION(事务) 是保障数据完整性和一致性的核心机制。它不仅帮助我们处理“转账”这类关键操作,还在高并发环境下维护了系统的稳定性。

通过理解事务的 ACID 特性、掌握 BEGIN / COMMIT / ROLLBACK / SAVEPOINT 等命令,以及合理设置隔离级别,我们可以编写出更健壮、更可靠的数据库应用。

记住:事务不是“魔法”,而是责任。每一次 COMMIT 都意味着对数据完整性的承诺。每一次 ROLLBACK 都是对错误的纠正。

在实际开发中,养成“事务最小化、显式控制、及时提交”的习惯,是每个合格开发者的基本素养。当你能熟练驾驭 PostgreSQL TRANSACTION(事务)时,你就真正掌握了数据库操作的本质。