MVCC不检查并发写冲突,因其设计目标仅为解决读-写冲突,而非写-写冲突;它确保事务读取快照,但UPDATE默认基于最新版本执行且不校验版本是否已变更,故无法防止第二类更新丢失。

为什么 MVCC 本身不检查并发写冲突
MVCC 的设计目标是解决「读-写」冲突,不是「写-写」冲突。它让每个事务看到自己开始时刻的快照,但所有事务更新时都基于同一行最新版本(或当前版本链头)执行 UPDATE,而不会自动比对“我读到的旧值是否已被别人改过”。换句话说:SELECT 是快照读(无锁),UPDATE 却默认是当前读(但不加锁,除非显式指定)。两个事务各自读到相同旧值 → 各自计算新值 → 各自提交 → 后提交者覆盖前提交者。
UPDATE 操作在 RR/RC 下默认不加锁,也不校验版本
在 REPEATABLE READ 或 READ COMMITTED 隔离级别下,一条裸 UPDATE ... WHERE id = ? 不会触发行锁,也不会检查该行自你上次 SELECT 以来是否被修改过。InnoDB 只保证你读到的是快照,不保证你写的依据没过期。
- 如果你用的是快照读(比如普通
SELECT)拿到balance = 100,然后执行UPDATE account SET balance = 150 WHERE id = 1,InnoDB 不会回溯验证“这行的原始值真是 100 吗?” - 另一个事务可能刚把
balance改成 120 并提交,你的 UPDATE 仍会成功写入 150 —— 120 就丢了。 - 这就是典型的“第二类更新丢失”,MVCC 完全不拦截它。
真正能阻断更新丢失的只有两类机制
靠 MVCC 自己做不到,必须额外引入控制手段:
-
SELECT ... FOR UPDATE:强制当前读 + 行级排他锁,后续UPDATE会被阻塞或等待,天然串行化写路径 - 乐观锁(如
version字段):在UPDATE的WHERE子句里显式校验,例如UPDATE t SET val=..., version=version+1 WHERE id=1 AND version=5;若 version 已变,影响行数为 0,应用层需重试 - 提升到
SERIALIZABLE隔离级别:MySQL 会自动将普通SELECT转为SELECT ... LOCK IN SHARE MODE,但性能代价大,一般不用
容易忽略的关键点:隔离级别 ≠ 写安全
很多人误以为设成 REPEATABLE READ 就万事大吉 —— 它确实防止了不可重复读和幻读,但对更新丢失毫无防护。MVCC 的“多版本”只服务于读一致性,写操作依然直面底层行数据。真正决定是否丢更新的,不是你用了什么隔离级别,而是你 UPDATE 前有没有锁住那行,或者有没有带条件校验。


















