大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
事务与 MVCC
这一课你将能做到
- 用
BEGIN/COMMIT/ROLLBACK包住多步更新,并演示一次回滚 - 说出 PG 默认隔离级别 Read Committed 与 MySQL InnoDB 默认 Repeatable Read 的差异直觉
- 用两会话实验理解「写等待」与 MVCC「读不挡写」的基本图像
为什么要学
转账、改库存、发布课时状态——多语句必须成功一起提交或一起失败。PostgreSQL 用 MVCC(多版本并发控制)实现事务:读通常不阻塞写,写冲突用行锁或更高隔离级别处理。
你在 sql-transaction 已学 ACID;本课对齐官方 Transaction Isolation 与 MVCC 概念,并为 Spring @Transactional 换库时的默认隔离差异打预防针。
先建立图像
会话 A 会话 B
BEGIN BEGIN
UPDATE 行 X … SELECT 行 X → 读到已提交旧版(RC 下按语句快照)
(未提交) UPDATE 行 X → 等待 A 的行锁
COMMIT (A 提交后)B 继续 / 或按隔离级别失败
| 级别 | PostgreSQL 要点 |
|---|---|
| Read Committed(默认) | 每条语句看到语句开始时已提交数据 |
| Repeatable Read | 事务内重复读更一致;可有序列化异常需重试 |
| Serializable | 更强;冲突时 serialization_failure |
MySQL InnoDB 默认常是 Repeatable Read;不要假设两库默认相同。
跟着做
步骤 1:确认 autocommit 习惯
docker exec -it pg-dev psql -U codeforge -d codeforge_pg
-- 单条默认立即提交
UPDATE app.cf_track SET sort_order = sort_order
WHERE slug = 'sql-essentials';
psql 默认自动提交。显式事务必须 BEGIN。
步骤 2:一次可回滚的事务
BEGIN;
UPDATE app.cf_track
SET sort_order = sort_order + 100
WHERE category = 'language';
SELECT slug, sort_order FROM app.cf_track WHERE category = 'language';
ROLLBACK; -- 撤销本事务全部更改
SELECT slug, sort_order FROM app.cf_track WHERE category = 'language';
第二次 SELECT 应回到原值。再练一次 COMMIT 留下变更(可自行加回)。
步骤 3:查看与设置隔离级别
SHOW transaction_isolation;
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT COUNT(*) FROM app.cf_track;
-- 保持事务打开,另开会话插入一行后再在本会话 SELECT COUNT
-- RC:可能看到新行;RR:通常仍是事务开始时的快照行数
COMMIT;
用两个终端最直观(步骤 4)。
步骤 4:两会话写冲突
终端 A:
BEGIN;
UPDATE app.cf_track SET title = title || ' [A]'
WHERE slug = 'postgresql-essentials';
-- 先别 COMMIT
终端 B:
BEGIN;
UPDATE app.cf_track SET title = title || ' [B]'
WHERE slug = 'postgresql-essentials';
-- 观察:会等待 A
回到 A:COMMIT; —— B 通常随后完成(或按情况报错)。最后 B 也 COMMIT 或 ROLLBACK,并把 title 改回干净值。
步骤 5:感受长事务风险(只看勿久挂)
-- 开发环境观察锁(表 oid 写法示例)
SELECT pid, mode, granted
FROM pg_locks
WHERE relation = 'app.cf_track'::regclass;
长事务会妨碍 vacuum 回收旧版本 → 表膨胀。Web 请求事务应短;报表大事务要隔离到只读副本或分批。
步骤 6:联系 Spring
回忆:@Transactional 在方法边界开关事务,与底层是 PG 还是 MySQL 无关。换库时重点检查:
- 默认隔离是否与你假设一致
- 死锁/序列化失败是否要重试
见 sb-transaction。
讲清楚要点
- 因为 MVCC 保留行版本,所以 普通
SELECT一般不加锁挡写;写同一行仍要排队。 - 默认 Read Committed:语句级快照;业务若需「整笔事务所见一致」,显式提高隔离或在应用层避免依赖幻觉。
- ROLLBACK 是一等公民:探索性改数先开事务,看结果再决定提交。
- Serializable 不是免费的:冲突时事务 abort,应用要捕获并重试。
- 短事务:减少锁等待与旧版本堆积;与 MySQL 长事务拖垮 undo 的教训同构。
- DDL 与事务:多数 DDL 可事务化,但锁级别高;生产结构变更走迁移窗口,不在高峰手搓。
你可能会懵的地方
-
「快照」是整库复制吗?
不是。是可见性规则:你的事务能看见哪些行版本。 -
为什么 RR 下有时仍要处理异常?
PG 的 RR/Serializable 在写冲突时可能抛错,需重试——与「永远不失败」的想象不同。 -
autocommit 关了吗?
psql 默认开。BEGIN块内才是多语句事务。JDBC 由连接/框架管理。 -
读已提交会不会脏读?
标准脏读(读到未提交)在 PG 已提交级别下不会;要防的是并发下的不可重复与幻读等,按级别变化。 -
和 MySQL 课冲突吗?
ACID 相同;默认隔离与锁细节不同。换库读官方隔离章,别硬背一张表。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
开着 BEGIN 去吃饭 | 锁与膨胀 | 事务短小;用完即提交/回滚 |
| 假定 PG 默认也是 RR | 并发行为与预期不符 | SHOW transaction_isolation |
| 捕获异常后不重试 serialization_failure | 用户看到随机失败 | 按官方建议有限次重试 |
| 在事务里跑超长分析 SQL 占主库 | 拖垮 OLTP | 拆分、超时、只读实例 |
无 WHERE 的 UPDATE 后才想起来 | 全表被改 | 先 BEGIN + SELECT 确认再提交 |
自测三问
- psql 下怎样才进入多语句事务?默认单条语句提交吗?
- PG 默认隔离级别是什么?与本站 MySQL 常见默认有何不同?
- 两会话更新同一行时,后到的会话通常先观察到什么现象?
小结一句话
PostgreSQL 用 MVCC + 标准隔离级别保证 ACID;默认 Read Committed,短事务、必要时显式升级并处理重试。