找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4757

积分

0

好友

617

主题
发表于 3 小时前 | 查看: 6| 回复: 0

MVCC 多版本并发控制,读不加锁,读写互不阻塞。

既然读都不用加锁了,数据库的并发问题不就解决了?那为什么 MySQL 还要搞出 READ UNCOMMITTEDREAD COMMITTEDREPEATABLE READSERIALIZABLE 这四种隔离级别?直接一把梭不就完了?

01. MVCC 到底做了什么

InnoDB 的每一行数据都藏着两个隐藏字段:

  • DB_TRX_ID:最后修改这行数据的事务 ID
  • DB_ROLL_PTR:指向 undo log 中这行的上一个版本

每次 UPDATE 一行数据,旧版本不会被覆盖,而是写进 undo log,通过指针串成一条 版本链

InnoDB MVCC版本链与undo log机制示意图

当一个事务执行 SELECT 时,InnoDB 不是直接读当前版本,而是生成一个叫 ReadView 的快照,用它来判断版本链上哪个版本对当前事务 可见

ReadView 里记录了当前所有活跃(还没提交)的事务 ID 列表。判断规则简单粗暴:

  • 这个版本的 trx_id 在我事务开始前就已经提交了 → 可见
  • 这个版本的 trx_id 是我之后才开始的事务 → 不可见,沿着版本链往下找
  • 这个版本的 trx_id 还在活跃列表里(没提交) → 不可见,继续往下找

到这里一切看起来都很完美:读历史版本,写当前版本,读写完全不冲突。

02. 那为什么还需要隔离级别?

因为 我应该看到哪个版本 这个问题,不同业务场景的答案不一样。

MVCC 只是提供了一种读历史版本的机制,但具体读哪个历史版本,取决于 ReadView 的创建策略。而这个策略,正是隔离级别在控制的东西。

READ COMMITTED

每次 SELECT 都重新创建新的 ReadView。

READ COMMITTED隔离级别ReadView创建流程

RC 级别下,每次 SELECT 都重新拍一张快照。如果两次 SELECT 之间有其他事务提交了修改,第二次读就能看到新数据,这就是所谓的 不可重复读——同一个事务里两次读同一行,结果不一样。

REPEATABLE READ

整个事务只创建一次 ReadView。

REPEATABLE READ隔离级别ReadView复用机制

RR 级别下,事务第一次 SELECT 的时候创建 ReadView,后续所有 SELECT 都复用同一个快照。不管外面的世界怎么变,你在这个事务里看到的数据永远一致。

RC 和 RR 用的是完全一样的 MVCC 机制,区别仅仅在于 ReadView 什么时候创建。一个是每次读都重新拍快照,一个是整个事务只拍一次。就这一个时机差异,造就了两种截然不同的隔离效果。

03. READ UNCOMMITTED 为什么不用 MVCC?

READ UNCOMMITTED 的语义是,可以读到其他事务还没提交的修改

既然连未提交的都能读,那直接读版本链最上面的当前版本就完事了,根本不需要创建 ReadView 去过滤。MVCC 的意义在于屏蔽不该看到的版本,如果你什么都想看,那 MVCC 就是多此一举。

所以 READ UNCOMMITTED 不是"不能用 MVCC",而是用了也没意义。

04. SERIALIZABLE 为什么也不用 MVCC?

SERIALIZABLE 走的是另一个极端,所有读操作都加共享锁。

-- 在 SERIALIZABLE 级别下,这条普通 SELECT
SELECT * FROM user WHERE id = 1;
-- 会被自动转换成
SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE;

读也加锁意味着读写之间会互相阻塞,跟 MVCC 的设计目标(读写不阻塞)完全背道而驰。SERIALIZABLE 放弃了 MVCC 的并发优势,换来的是最强的一致性保证,事务之间完全串行执行。

05. 一张图看清关系

MVCC与四种隔离级别关系图

06. MVCC 管不了写

MVCC 只解决了读的问题(快照读),写的时候依然需要锁。

InnoDB 里有两种读:

快照读(Snapshot Read) :普通 SELECT,走 MVCC,不加锁。

当前读(Current Read)SELECT ... FOR UPDATEUPDATEDELETEINSERT。这些必须读到当前最新版本(不然 UPDATE 可能基于过时数据修改),所以不走 MVCC,走行锁。

-- 快照读:走 MVCC,读的可能是历史版本,不加锁
SELECT * FROM account WHERE id = 1;

-- 当前读:加排他锁,读最新版本
SELECT * FROM account WHERE id = 1 FOR UPDATE;

-- 当前读:UPDATE 内部会先读取当前最新值
UPDATE account SET balance = balance - 100 WHERE id = 1;

InnoDB 在 RC 和 RR 级别下的实际策略是:

读用 MVCC → 无锁快照读,不阻塞写

写用行锁 → 互斥,保证数据安全

读写之间不冲突(读的是历史版本,写的是当前版本),写写之间通过行锁互斥。这才是 InnoDB 并发性能好的真正原因,不是单靠 MVCC 或者单靠锁,而是两者的分工配合。

07. 说在最后

MVCC 是一个底层的并发控制工具,解决的是读操作如何不加锁地获取一致数据这个问题。

但一致到什么程度?是只看已提交的最新数据(RC),还是整个事务期间数据不变(RR),还是连未提交的都能看(RU),还是读也加锁保证绝对串行(Serializable)?这是业务层面的选择,由隔离级别来控制。

MVCC 提供能力,隔离级别定义策略,它们压根不是同一层面的东西。

就这点事而已!更多关于数据库底层机制的实战拆解,欢迎到 云栈社区 一起聊。




上一篇:Oracle 在线重定义表结构:DBMS_REDEFINITION 底层依赖什么机制?
下一篇:火山引擎 TLS AgentLoop 全链路可观测:LLM 应用会话透明复盘
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-9-11 22:35 , Processed in 0.310981 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表