mysql从零单排之快照读与当前读

了解完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 同时提供两种机制,而不是一刀切。

相关推荐
candyTong8 小时前
Claude Code 的 Edit 工具是怎么工作的
javascript·后端·架构
GetcharZp9 小时前
GitHub 2.4 万 Star!D2 正在重新定义程序员画图方式
后端
zhangxingchao11 小时前
多 Agent 架构到底怎么选?从 Claude Agent Teams、Cognition/Devin 到工程落地原则
前端·人工智能·后端
IT_陈寒11 小时前
SpringBoot那个自动配置的坑,害我排查到凌晨三点
前端·人工智能·后端
ServBay11 小时前
OpenCode 和它的7款必备插件
后端·github·ai编程
ping某11 小时前
逐字节拆解 tcpdump
后端
阿凡98073011 小时前
花 100 dollar,用 Claude 打通 EasyEDA&Fusion 双向同步
后端·程序员
irving同学4623811 小时前
从零搭建生产级 RAG:Embedding、Chunking、Hybrid Search 与 Reranker
前端·后端
她的男孩12 小时前
从零搭一个企业后台,为什么我把能力拆成 Starter 和 Plugin
java·后端·架构
胡志辉12 小时前
本地 AI 编码助手从 0 配起来:先选模型,再接 Ollama、VS Code、Claude Code 和 Codex
前端·后端