没有隔离会发生什么
学习 MVCC 之前,先来看为什么需要它。
上篇文章有讲到 ACID,重点是 隔离性,如果没有隔离会出现什么问题
- 脏读
这里 事务2 已经基于 已扣款 也就是 余额 =1000 - 500 = 500 去做决策了,但是 A 的余额=1000
更严重的例子:
css
事务A 初始余额1000,执行了 update 余额 = 10000,没有commit
事务B 读到余额 = 10000,允许取款8000
事务A 回滚,余额还是1000
后果:银行少8000块
- 不可重复读
这里,同一事务内,同一行数据前后两次读的值不同,这就是不可重复读。
更严重一点的例子:
ini
第一步:select stock from products where id=1 ->查出来 stock=10
判断 10> 0,够卖
第二步:update stock = stock -1 where id=1
但是,这两步中间有人插进来了,给 stock 改成了 0
->你依旧基于 stock=10 做决策 -> 卖超了
- 幻读
这里,并不是某一行的值变了,而是符合条件的行数变了
更严重的例子:
sql
第一步:SELECT COUNT(*) FROM admins WHERE role='super' → 1 人
判断:只有一个超级管理员
有人 insert 了一个新的 super admin
第二步:执行某个"只允许唯一超管操作"的逻辑
-> 实际有两个超管,安全假设被打破
这些现象说明了隔离性为什么重要。PostgreSQL 支持四个隔离级别:
- Read Uncommitted(读未提交)
- Read Committed(读已提交)
- Repeatable Read(可重复读)
- Serializable(串行化)
通常隔离越强,冲突、等待或重试的成本也可能越高,但它们不是简单的线性反比:

首先 PostgreSQL 的默认级别是 Read Committed(读已提交)。其次,你可以设置 Read Uncommitted,但是 PostgreSQL 会按 Read Committed 来处理,所以在 PostgreSQL 里不会发生脏读。
上面的脏读例子是数据库通用概念,用来说明"没有隔离"会有多危险,并不是 PostgreSQL 实际允许的行为。
PostgreSQL 到底怎么控制"看见什么数据"
那么 Read Committed 到底怎么做的?
规则只有一条:每条 SQL 语句执行的瞬间,只能看到在那一瞬间之前已经 COMMIT 的数据

那么 Repeatable Read 怎么防住不可重复读?
规则变了一条: 整个事务会固定使用同一份快照。你可以先理解成,事务第一次查询时看到什么,后面普通查询就继续看这份结果,无论别人后来 COMMIT 了什么。

但是 Repeatable Read 并没有把整张表锁住,也没有阻止别人修改。别的事务照样可以 insert、update、delete,完全不受影响。
它做的只是让你自己的普通读取始终看同一份快照,所以后来插入的符合条件的新行,你也不会突然看见。至于库存、唯一超级管理员这类业务规则,仍然要配合行锁、唯一约束,或者 Serializable 来保证。
那怎么做到"两次读的结果一样"的?
MVCC:让读和写各看各的版本
那么 MVCC 终于出来了,它解决了并发读写的根本矛盾。
数据库面对两个对立需求:
- 读的想要稳定的数据 -- 别在我看的时候改
- 写的人想要马上改 -- 别让我等
如果用 锁 来解决,写的时候锁住,读的人排队等,这是很安全,但慢的不可接受
MVCC = 多版本并发控制 (Multi-Version Concurrency Control)
解决方法就是:给数据拍快照
做法如下:
- 先有一个原始数据:
2. 执行 update price = 200 (旧版本标记删除 + 插入新版本)

两个版本同时存在,这就是 多版本 的意思
由快照决定你看到哪个版本
text
事务 75(begin 在 txn 80 之前开始)
->快照冻结点 txn 80 之前
->看到 price = 100
事务 90(begin 在 txn80 commit 之后)
->快照冻结点 txn 80 之后
->看到price =200
MVCC 不够时,还是要用锁
MVCC 主要解决的是"读不必等写,写也不必等读"。但在 MVCC 不够用的情况下,还是要用到锁,比如:
写并发--两个事务同时要改同一行
那 PostgreSQL 就必须用 锁 了

死锁:互相等对方

那么怎么处理?难不成一直等下去?
不会。PostgreSQL 发现事务等待超过 deadlock_timeout(默认 1 秒)后,会检查有没有死锁:
- 发现A 和 B形成等待环路
- 选择其中一个事务中止
- rollback 该事务,释放它持有的锁
- 另一个事务继续执行
那我们如何避免死锁的出现?
先看死锁是怎么形成的:
bash
事务A:先锁 id=1,再锁 id=2
事务B:先锁 id=2,再锁 id=1
可以看出,事务B反过来先给 id=2 给锁住了
如果是这样做的话
bash
事务A:先锁 id=1,再锁 id=2
事务B:先锁 id=1,再锁 id=2
事务B会先等事务A释放 id=1 的锁,不会形成交叉等待。
所以答案就是:所有事务按固定顺序访问资源(比如按 id 从小到大)。顺序一致,等待环路就不容易形成。
总结
最后,如果只用一句话理解 MVCC,那就是:它不是阻止别人修改数据,而是让不同事务在并发时,各自看到适合自己的数据版本。
Read Committed 让每条 SQL 看执行时已经提交的数据;Repeatable Read 让同一个事务的普通读取固定在同一份快照上;当两个事务真的要改同一行,或者业务规则不能接受并发冲突时,数据库还是会回到锁和事务控制。
所以 MVCC 和锁不是谁取代谁。MVCC 负责让大多数读写并行起来,锁负责处理不能同时进行的那一小部分关键操作。