了解完MVCC会不会有这样的疑问:MVCC实现了读不阻塞写,写不阻塞读的高效并发控制,写的时候读是不是拿到实时数据,读的时候可能拿到的不是实时写的数据?
关键区分:两种读操作
MySQL InnoDB 实际上有两种读模式,MVCC 只适用于其中一种:
| 读类型 | 名称 | 实现方式 | 是否阻塞写 | 数据一致性 |
|---|---|---|---|---|
| 快照读 | Snapshot Read (Consistent Read) | MVCC,读历史版本 | 不阻塞 | 事务开始时的快照 |
| 当前读 | Current Read (Locking Read) | 加锁读最新版本 | 会阻塞 | 最新已提交数据 |
sql
-- 快照读(普通 SELECT)-- 使用 MVCC
SELECT * FROM user WHERE id = 1;
-- 当前读(加锁 SELECT)-- 不使用 MVCC,直接读最新版本
SELECT * FROM user WHERE id = 1 FOR UPDATE; -- 排他锁
SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE; -- 共享锁
你的问题:
答案取决于业务场景:
场景1:读历史版本就够了(大多数场景)
sql
银行查询账户余额(只读场景):
事务A(转账):UPDATE account SET balance = 900 WHERE id = 1; -- 扣100
事务B(查询):SELECT balance FROM account WHERE id = 1; -- 读快照
├─► 事务B 读到的 1000元 是"过时"的吗?技术上是的
├─► 但这是事务B 启动时的准确快照,业务上完全合理
└─► 用户只是查余额,不需要看到实时变化(实时变化可能还在处理中)
场景2:必须读最新数据(需要当前读)
sql
库存扣减(不能超卖):
事务A:UPDATE stock SET count = count - 1 WHERE id = 1; -- 剩9件
事务B:SELECT count FROM stock WHERE id = 1 FOR UPDATE; -- 必须知道最新值!
├─► 事务B 用 FOR UPDATE,强制读最新版本(当前读)
├─► 如果 count 已经是 0,事务B 就知道卖完了
└─► 这会阻塞直到事务A提交,但保证了业务正确性
一致性模型对比
scss
┌─────────────────────────────────────────────────────────────┐
│ 一致性光谱 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 最终一致性 ◄────────────────────────────► 强一致性 │
│ │
│ 快照读(MVCC) 当前读(加锁) │
│ ├─ 读历史版本 ├─ 读最新版本 │
│ ├─ 可能"过时"但自洽 ├─ 实时但可能阻塞 │
│ └─ 适合:报表、查询 └─ 适合:金融交易、库存 │
│ │
└─────────────────────────────────────────────────────────────┘
为什么 MVCC 这样设计?
sql
┌─────────────────────────────────────────────────────────┐
│ 数据库设计哲学:不同场景需要不同的一致性级别 │
├─────────────────────────────────────────────────────────┤
│ │
│ 场景1:淘宝商品列表页 │
│ ├─ 100万人同时浏览 │
│ ├─ 库存数字差几个没关系 │
│ └─ 用 MVCC 快照读 → 高并发,不阻塞商家改库存 │
│ │
│ 场景2:下单扣库存 │
│ ├─ 必须精确知道当前库存 │
│ ├─ 不能超卖 │
│ └─ 用当前读 FOR UPDATE → 阻塞等待,保证正确性 │
│ │
│ 同一系统,两种策略,各取所需 │
│ │
└─────────────────────────────────────────────────────────┘
代码示例对比
sql
-- 假设表:account(id, balance)
┌─────────────────────────────────────────────────────────────┐
│ 事务A(转账) │
├─────────────────────────────────────────────────────────────┤
│ START TRANSACTION; │
│ -- 扣减转账账户 │
│ UPDATE account SET balance = balance - 100 WHERE id = 1; │
│ -- 此时 id=1 的 balance 内存中有新版本,但未提交 │
│ │
│ -- 做一些其他处理(耗时较长) │
│ ... │
│ │
│ COMMIT; │
└─────────────────────────────────────────────────────────────┘
│
│ 并发执行
▼
┌─────────────────────────────────────────────────────────────┐
│ 事务B(查询余额)- 快照读 │
├─────────────────────────────────────────────────────────────┤
│ START TRANSACTION; │
│ SELECT balance FROM account WHERE id = 1; -- 读快照 │
│ -- 结果:1000(事务A修改前的值) │
│ -- 原理:TRX_ID 判断,事务A未提交,对事务B不可见 │
│ COMMIT; │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 事务C(必须确认转账结果)- 当前读 │
├─────────────────────────────────────────────────────────────┤
│ START TRANSACTION; │
│ SELECT balance FROM account WHERE id = 1 FOR UPDATE; │
│ -- 结果:等待... 直到事务A提交后返回 900 │
│ -- 原理:加排他锁,强制读最新版本,阻塞等待 │
│ COMMIT; │
└─────────────────────────────────────────────────────────────┘
总结
| 你的顾虑 | MVCC 的答案 |
|---|---|
| 「写的时候读,数据不准确」 | 快照读 故意读"旧"数据,换取不阻塞 |
| 「但我需要准确数据」 | 用 当前读 (FOR UPDATE),牺牲并发换准确 |
| 「到底准不准」 | 快照读在事务开始时是一致的、自洽的,只是不是最新的 |
MVCC 不是万能药,而是给开发者选择权:
要并发?用快照读。要准确?用当前读。
这也是为什么 InnoDB 同时提供两种机制,而不是一刀切。