事务与 ACID:全有或全无
从这一卷起,我们进入 PostgreSQL 最深、也最见功力的地带:当很多人同时读写同一份数据,怎么保证谁都不被骗。第一块基石是事务——它把一串操作捆成一个「要么全成、要么全不成」的整体。这是数据库对「正确性」最庄重的一次「当真」。
转账问题:为什么需要事务
经典场景:A 给 B 转 100 块。这在数据库里是两步:
UPDATE account SET balance = balance - 100 WHERE id = 'A'; UPDATE account SET balance = balance + 100 WHERE id = 'B';
要命的问题是:如果第一步成功、第二步之前数据库崩了(断电、进程被杀、网络断),会怎样?A 的钱扣了,B 的钱没加——凭空蒸发了 100 块。这种「做了一半」的状态,是绝对不能接受的。
事务就是解药。把两步包进一个事务,它们就成了一个不可分割的整体:
BEGIN; UPDATE account SET balance = balance - 100 WHERE id = 'A'; UPDATE account SET balance = balance + 100 WHERE id = 'B'; COMMIT; -- 到这里,两步一起生效;中途任何闪失,两步一起作废
COMMIT 之前,这两步对别人都不可见;COMMIT 那一刻,它们原子地一起生效。如果中途出错,一句 ROLLBACK(或崩溃后数据库自动回滚),两步一起消失,就像什么都没发生。永远不会停在「做了一半」的状态。
ACID:事务的四条承诺
事务给的保证,概括成四个字母 ACID:
| 字母 | 名字 | 意思 |
|---|---|---|
| A | 原子性 Atomicity | 全有或全无。事务里的操作,要么全部生效,要么全部不生效。 |
| C | 一致性 Consistency | 事务把数据库从一个合法状态带到另一个合法状态——所有约束(卷 III)在事务边界上都成立。 |
| I | 隔离性 Isolation | 并发的事务互不干扰,各自像独自运行一样。这一条最微妙,下一章、下下章专讲。 |
| D | 持久性 Durability | 一旦 COMMIT,数据就落地了,哪怕紧接着断电也不丢。 |
这四条里,原子性解决「做了一半」,持久性解决「说存了就真存了」,一致性是约束系统的延伸,而隔离性——「并发时互不干扰」——是整卷 IV 的主角,也是 PostgreSQL 用 MVCC 精妙解决的那个问题。
PostgreSQL 怎么做到「COMMIT 了就不丢」?靠 WAL(Write-Ahead Log,预写日志)。改数据之前,它先把「我要做什么改动」写进一个只追加的日志文件并刷到磁盘,然后才慢慢改真正的数据页。COMMIT 的本质,就是「这个事务的 WAL 记录已经安全落盘」。万一此后崩溃,重启时按 WAL 重放一遍,已提交的改动一条不少地恢复。WAL 还是主从复制、时间点恢复(PITR)的基础——卷 VI 会再提。
autocommit:你其实一直在用事务
就算你从没写过 BEGIN,你也一直在用事务。默认情况下,每一条单独的 SQL 语句,都自动被包在一个自己的事务里(autocommit)。一条 UPDATE 影响一万行——这一万行要么全改、要么全不改,因为它本身就是一个原子事务。
BEGIN ... COMMIT 的意义,是把多条语句捆成一个原子单元。什么时候需要显式事务?当「几步操作必须共同成败」时——转账、下单扣库存、任何「改了这个就必须改那个」的成对操作。
SAVEPOINT:事务里的存档点
一个长事务里,你可能想「试探性地做几步,不行就退回到某处,而不是整个事务作废」。SAVEPOINT 就是事务内的存档点:
BEGIN; INSERT INTO orders ...; SAVEPOINT sp1; -- 存档 UPDATE stock ...; -- 试探性操作 ROLLBACK TO SAVEPOINT sp1; -- 只回退到 sp1,前面的 INSERT 还在 UPDATE stock ...; -- 换个方式再来 COMMIT;
这在需要「部分回滚」的复杂逻辑里很有用。很多语言的数据库驱动,也用 SAVEPOINT 来实现「嵌套事务」的假象。
可回滚的 DDL:一个让 MySQL 用户愣住的能力
这里是本章的「当真」高光。在 PostgreSQL 里,连建表、改表这样的结构变更(DDL),都能包在事务里,也能回滚:
BEGIN; CREATE TABLE t (id int); ALTER TABLE orders ADD COLUMN note text; -- 发现方案不对…… ROLLBACK; -- 这两条 DDL 一起撤销,就像从没建过表、加过列
这意味着数据库迁移(migration)可以是原子的:一个迁移脚本包含建表、加列、改数据好几步,用一个事务包住——中途任何一步失败,整个迁移干净地回滚,数据库回到迁移前的状态,绝不会卡在「表建了一半、列加了三个还差两个」的残破中间态。这对生产环境的安全变更,价值巨大。
- MyISAM 根本没有事务:MySQL 早年默认的 MyISAM 引擎不支持事务,一条语句崩在中间就是崩在中间。今天要用事务必须用 InnoDB(现代默认)。PostgreSQL 只有一种存储,事务是与生俱来、无法关闭的。
- DDL 不可回滚:这是最大的差异。在 MySQL 里,
CREATE TABLE、ALTER TABLE等 DDL 会触发隐式提交——你事务里前面的改动会被提前COMMIT,DDL 本身也无法ROLLBACK。所以 MySQL 的迁移中途失败,很可能留下一个半残的 schema,要手动收拾。PostgreSQL 的迁移可以是全有或全无。 - autocommit、SAVEPOINT、WAL 的思路:两家都有(MySQL 的 WAL 叫 redo log),基本概念相通。
这一章的一句话
事务把多步操作捆成「全有或全无」的原子单元(ACID 的 A),靠 WAL 保证 COMMIT 即持久(D);PostgreSQL 连 DDL 都能包进事务回滚,让数据库迁移可以干净地全成或全不成。
ACID 里最微妙的那个字母是 I——隔离性。当两个事务同时读写同一行,PostgreSQL 用一套叫 MVCC 的机制,让它们互不阻塞、又各自看到一致的世界。下一章,我们用真引擎,看一行数据怎么裂成两个版本。