第六章 事务与并发控制
现实中,很多操作需要"要么全做完、要么全不做"。事务就是保障这一点的机制。本章帮你理解事务的用法和 ACID 原则。
6.1 为什么需要事务
经典场景:银行转账
小明要给小红转 100 元,需要两步操作:
步骤 1:从小明的账户扣 100 元
步骤 2:给小红的账户加 100 元如果步骤 1 执行成功了,步骤 2 因为系统故障失败了——小明的钱扣了,小红没收到,100 元凭空消失。
事务的作用:把这两步绑定在一起。如果任何一步失败,全部撤销,就像什么都没发生过。
6.2 事务的基本用法
准备测试数据
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 — 提交事务
BEGIN; -- 开始事务
UPDATE accounts SET balance = balance - 100 WHERE name = '小明';
UPDATE accounts SET balance = balance + 100 WHERE name = '小红';
COMMIT; -- 提交事务:两条 UPDATE 一起生效执行 COMMIT 后,两条更新才真正写入数据库。
SELECT * FROM accounts;
-- 小明: 900.00, 小红: 600.00BEGIN / ROLLBACK — 回滚事务
BEGIN;
UPDATE accounts SET balance = balance - 200 WHERE name = '小明';
-- 这时候发现转错人了
ROLLBACK; -- 回滚:撤销 BEGIN 之后的所有操作SELECT * FROM accounts;
-- 小明仍然是 900.00,没有变化事务中出现错误
如果事务中某条语句执行失败,PostgreSQL 会将该事务标记为"异常状态",后续语句都无法执行,只能 ROLLBACK:
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 — 事务中的"存档点"
当事务中有多个步骤时,可以设置存档点,只回滚到某个存档点而非回滚整个事务:
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(原子性)
事务中的所有操作,要么全部成功,要么全部失败。
没有"执行了一半"的中间状态。就像原子不可分割一样。
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 标准更严格。
查看和设置隔离级别
-- 查看当前隔离级别
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 语句就是一个单独的小事务,执行完自动提交。
-- 这两条语句是两个独立事务,各自自动提交
UPDATE accounts SET balance = balance - 100 WHERE name = '小明';
-- ← 自动 COMMIT
UPDATE accounts SET balance = balance + 100 WHERE name = '小红';
-- ← 自动 COMMIT上面的写法存在风险:如果第一条成功、第二条失败,数据就不一致了。
所以,当多个操作需要保持原子性时,必须显式使用 BEGIN ... COMMIT。
6.6 实战练习
-- 准备数据(如果还没有)
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 → 操作 → COMMIT 或 ROLLBACK |
| SAVEPOINT | 事务内的存档点,支持部分回滚 |
| ACID | 原子性、一致性、隔离性、持久性 |
| 隔离级别 | 默认 Read Committed,按需提升 |
| 自动提交 | 不写 BEGIN 时每条语句自动提交 |
核心经验:涉及多步操作且需要保持数据一致性时,永远用
BEGIN ... COMMIT包裹。