卷 IV · 事务CH 15深度 15/24

事务与 ACID:全有或全无

从这一卷起,我们进入 PostgreSQL 最深、也最见功力的地带:当很多人同时读写同一份数据,怎么保证谁都不被骗。第一块基石是事务——它把一串操作捆成一个「要么全成、要么全不成」的整体。这是数据库对「正确性」最庄重的一次「当真」。

事务ACIDSAVEPOINT可回滚的 DDL

转账问题:为什么需要事务

经典场景: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 精妙解决的那个问题。

◆ 持久性靠 WAL:先写日志,再改数据

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)可以是原子的:一个迁移脚本包含建表、加列、改数据好几步,用一个事务包住——中途任何一步失败,整个迁移干净地回滚,数据库回到迁移前的状态,绝不会卡在「表建了一半、列加了三个还差两个」的残破中间态。这对生产环境的安全变更,价值巨大。

⇄ 对照 MySQL
  • MyISAM 根本没有事务:MySQL 早年默认的 MyISAM 引擎不支持事务,一条语句崩在中间就是崩在中间。今天要用事务必须用 InnoDB(现代默认)。PostgreSQL 只有一种存储,事务是与生俱来、无法关闭的。
  • DDL 不可回滚:这是最大的差异。在 MySQL 里,CREATE TABLEALTER 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 的机制,让它们互不阻塞、又各自看到一致的世界。下一章,我们用真引擎,看一行数据怎么裂成两个版本。