Skip to content

第六章 事务与并发控制

现实中,很多操作需要"要么全做完、要么全不做"。事务就是保障这一点的机制。本章帮你理解事务的用法和 ACID 原则。


6.1 为什么需要事务

经典场景:银行转账

小明要给小红转 100 元,需要两步操作:

步骤 1:从小明的账户扣 100 元
步骤 2:给小红的账户加 100 元

如果步骤 1 执行成功了,步骤 2 因为系统故障失败了——小明的钱扣了,小红没收到,100 元凭空消失。

事务的作用:把这两步绑定在一起。如果任何一步失败,全部撤销,就像什么都没发生过。


6.2 事务的基本用法

准备测试数据

sql
CREATE TABLE accounts (
    id      INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name    VARCHAR(50) NOT NULL,
    balance NUMERIC(10,2) NOT NULL CHECK (balance >= 0)
);

INSERT INTO accounts (name, balance) VALUES
    ('小明', 1000.00),
    ('小红', 500.00);

BEGIN / COMMIT — 提交事务

sql
BEGIN;    -- 开始事务

UPDATE accounts SET balance = balance - 100 WHERE name = '小明';
UPDATE accounts SET balance = balance + 100 WHERE name = '小红';

COMMIT;   -- 提交事务:两条 UPDATE 一起生效

执行 COMMIT 后,两条更新才真正写入数据库。

sql
SELECT * FROM accounts;
-- 小明: 900.00, 小红: 600.00

BEGIN / ROLLBACK — 回滚事务

sql
BEGIN;

UPDATE accounts SET balance = balance - 200 WHERE name = '小明';
-- 这时候发现转错人了

ROLLBACK;   -- 回滚:撤销 BEGIN 之后的所有操作
sql
SELECT * FROM accounts;
-- 小明仍然是 900.00,没有变化

事务中出现错误

如果事务中某条语句执行失败,PostgreSQL 会将该事务标记为"异常状态",后续语句都无法执行,只能 ROLLBACK:

sql
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE name = '小明';
UPDATE accounts SET balance = balance - 99999 WHERE name = '小红';   -- 会触发 CHECK 约束失败
-- ERROR: new row for relation "accounts" violates check constraint

COMMIT;   -- 这里实际上无法提交
-- ERROR: current transaction is aborted, commands ignored until end of transaction block

ROLLBACK; -- 只能回滚

SAVEPOINT — 事务中的"存档点"

当事务中有多个步骤时,可以设置存档点,只回滚到某个存档点而非回滚整个事务:

sql
BEGIN;

UPDATE accounts SET balance = balance - 100 WHERE name = '小明';
SAVEPOINT sp1;    -- 创建存档点

UPDATE accounts SET balance = balance - 99999 WHERE name = '小红';
-- 哦不,出错了

ROLLBACK TO sp1;  -- 只回滚到存档点 sp1,小明的扣款保留

UPDATE accounts SET balance = balance + 100 WHERE name = '小红';  -- 换一条正确的操作

COMMIT;   -- 成功提交

6.3 ACID — 事务的四大特性

ACID 是衡量事务可靠性的四个标准,PostgreSQL 完全满足这四点。

A — Atomicity(原子性)

事务中的所有操作,要么全部成功,要么全部失败。

没有"执行了一半"的中间状态。就像原子不可分割一样。

sql
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE name = '小明';  -- 成功
UPDATE accounts SET balance = balance + 100 WHERE name = '小红';  -- 假如这步失败
COMMIT; -- 那么第一步的扣款也会被撤销

C — Consistency(一致性)

事务执行前后,数据始终满足所有约束和规则。

比如我们给 balance 设了 CHECK (balance >= 0),任何导致余额为负的操作都会被拒绝,不会出现不合法的数据。

I — Isolation(隔离性)

多个事务并发执行时,互相不受干扰,就像一个一个串行执行。

这是并发控制的核心话题,下一节展开。

D — Durability(持久性)

事务一旦 COMMIT 成功,数据就永久保存了,即使之后断电、系统崩溃,也不会丢失。

PostgreSQL 通过 WAL(Write-Ahead Logging)机制保障——先写日志,再写数据。即使崩溃,也能通过日志恢复。


6.4 隔离级别

当多个用户同时操作数据库时,可能会出现一些并发问题:

问题说明
脏读(Dirty Read)读到了别人还没提交的数据
不可重复读(Non-repeatable Read)同一个事务内两次读同一行,结果不一样
幻读(Phantom Read)同一个事务内两次相同查询,结果行数不一样

SQL 标准定义了四种隔离级别来应对这些问题:

隔离级别脏读不可重复读幻读性能
Read Uncommitted可能可能可能最高
Read Committed(PostgreSQL 默认)不会可能可能
Repeatable Read不会不会可能*
Serializable不会不会不会较低

*PostgreSQL 的 Repeatable Read 实际上也防止了幻读,比 SQL 标准更严格。

查看和设置隔离级别

sql
-- 查看当前隔离级别
SHOW default_transaction_isolation;
-- 输出:read committed

-- 为单个事务设置隔离级别
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- ... 你的操作 ...
COMMIT;

实际建议

  • 大多数情况下使用默认的 Read Committed 就够了
  • 如果需要"事务内多次读取结果一致",用 Repeatable Read
  • 如果需要最强一致性保障(比如金融系统),用 Serializable

6.5 PostgreSQL 中的自动提交

一个常见疑问:我平时没写 BEGIN,数据也能存进去啊?

这是因为 PostgreSQL 有自动提交(autocommit) 机制——如果你不显式写 BEGIN,每条 SQL 语句就是一个单独的小事务,执行完自动提交。

sql
-- 这两条语句是两个独立事务,各自自动提交
UPDATE accounts SET balance = balance - 100 WHERE name = '小明';
-- ← 自动 COMMIT
UPDATE accounts SET balance = balance + 100 WHERE name = '小红';
-- ← 自动 COMMIT

上面的写法存在风险:如果第一条成功、第二条失败,数据就不一致了。

所以,当多个操作需要保持原子性时,必须显式使用 BEGIN ... COMMIT


6.6 实战练习

sql
-- 准备数据(如果还没有)
DROP TABLE IF EXISTS accounts;
CREATE TABLE accounts (
    id      INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name    VARCHAR(50) NOT NULL,
    balance NUMERIC(10,2) NOT NULL CHECK (balance >= 0)
);
INSERT INTO accounts (name, balance) VALUES ('小明', 1000.00), ('小红', 500.00);

-- 练习 1:成功的转账事务
BEGIN;
UPDATE accounts SET balance = balance - 200 WHERE name = '小明';
UPDATE accounts SET balance = balance + 200 WHERE name = '小红';
COMMIT;
SELECT * FROM accounts;  -- 小明 800, 小红 700

-- 练习 2:失败的转账(余额不足)
BEGIN;
UPDATE accounts SET balance = balance - 9999 WHERE name = '小明';
-- ERROR: CHECK 约束失败
ROLLBACK;
SELECT * FROM accounts;  -- 小明仍然 800, 没变

-- 练习 3:使用 SAVEPOINT
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE name = '小明';  -- 小明 → 700
SAVEPOINT before_transfer;
UPDATE accounts SET balance = balance + 100 WHERE name = '不存在的人';  -- 没有影响任何行
ROLLBACK TO before_transfer;  -- 回退到存档点
UPDATE accounts SET balance = balance + 100 WHERE name = '小红';  -- 小红 → 800
COMMIT;
SELECT * FROM accounts;  -- 小明 700, 小红 800

本章小结

你学到了要点
事务语法BEGIN → 操作 → COMMITROLLBACK
SAVEPOINT事务内的存档点,支持部分回滚
ACID原子性、一致性、隔离性、持久性
隔离级别默认 Read Committed,按需提升
自动提交不写 BEGIN 时每条语句自动提交

核心经验:涉及多步操作且需要保持数据一致性时,永远用 BEGIN ... COMMIT 包裹。


下一章:第七章 PostgreSQL 特色功能

坚持是一种品格