
学习 MVCC 之前,先来看为什么需要它。
上篇文章有讲到 ACID,重点是 隔离性,如果没有隔离会出现什么问题

这里 事务2 已经基于 已扣款 也就是 余额 =1000 - 500 = 500 去做决策了,但是 A 的余额=1000
更严重的例子:
事务A 初始余额1000,执行了 update 余额 = 10000,没有commit
事务B 读到余额 = 10000,允许取款8000
事务A 回滚,余额还是1000
后果:银行少8000块

这里,同一事务内,同一行数据前后两次读的值不同,这就是不可重复读。
更严重一点的例子:
第一步:select stock from products where id=1 ->查出来 stock=10
判断 10> 0,够卖
第二步:update stock = stock -1 where id=1
但是,这两步中间有人插进来了,给 stock 改成了 0
->你依旧基于 stock=10 做决策 -> 卖超了

这里,并不是某一行的值变了,而是符合条件的行数变了
更严重的例子:
第一步:SELECT COUNT(*) FROM admins WHERE role='super' → 1 人
判断:只有一个超级管理员
有人 insert 了一个新的 super admin
第二步:执行某个"只允许唯一超管操作"的逻辑
-> 实际有两个超管,安全假设被打破
这些现象说明了隔离性为什么重要。PostgreSQL 支持四个隔离级别:
通常隔离越强,冲突、等待或重试的成本也可能越高,但它们不是简单的线性反比:


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

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

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


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

死锁:互相等对方

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