【MySQL】深入理解MVCC:MySQL如何实现非阻塞读?

深入理解MySQL的MVCC与非阻塞读

目录

前言:一个“神奇”的现象

🧩 一、什么是MVCC?核心思想

🔍 传统锁的痛点

💡 MVCC的解决方案

🏗️ 二、InnoDB中MVCC的实现:隐藏字段与Read View

🔹 1. 隐藏字段(Hidden Columns)

🔹 2. Undo Log(回滚日志)

🔹 3. Read View(读视图)

🔄 三、MVCC工作流程:如何判断数据可见性?

🔍 可见性判断算法

🧪 四、实战案例:揭秘“非阻塞读”

🎯 场景重现

🔍 五、快照读 vs 当前读

🎯 总结:MVCC核心要点


前言:一个“神奇”的现象

你是否遇到过这种情况?

-- 事务1(长时间运行)
START TRANSACTION;
SELECT SUM(balance) FROM accounts; -- 开始统计总余额 (耗时10秒)
-- ... 10秒后统计完成

-- 与此同时,事务2
UPDATE accounts SET balance = balance + 100 WHERE id = 1;
COMMIT; -- 立即完成

问题:事务2在事务1统计过程中修改了数据,事务1最终统计的总余额会包含这100元吗?

答案不会! 事务1看到的仍然是修改前的数据。

更神奇的是:事务1的SELECT语句并没有阻塞事务2的UPDATE操作!

👉 这就是 MVCC(Multi-Version Concurrency Control,多版本并发控制) 的魔力!它让读操作(SELECT无需加锁,也能保证数据的一致性视图,实现了读写不冲突,极大地提升了并发性能。


🧩 一、什么是MVCC?核心思想

🔍 传统锁的痛点

在没有MVCC的时代(如早期的数据库或MyISAM引擎):

  • SELECT 为了保证一致性,可能需要加S锁。
  • UPDATE 需要加X锁。
  • 结果:读写相互阻塞!一个长查询会阻塞所有写入,性能极差。

💡 MVCC的解决方案

MVCC的核心思想是:不直接覆盖旧数据,而是保存数据的多个版本

  • 每次更新数据时,不直接修改原记录,而是创建一个新版本
  • 每个事务在启动时,会看到一个基于当时状态的“快照”(Snapshot)
  • 事务只能看到在它启动之前已经提交的版本,以及它自己创建的版本。
  • 这样,读操作直接从“快照”中读取,完全不需要加锁,自然不会阻塞写操作。

类比:就像Git版本控制。你git checkout到某个历史提交(快照),你可以查看代码,但不会影响其他人正在git commit新代码。


🏗️ 二、InnoDB中MVCC的实现:隐藏字段与Read View

InnoDB通过 隐藏字段Read View 机制来实现MVCC。

🔹 1. 隐藏字段(Hidden Columns)

InnoDB自动为每行数据添加几个隐藏字段:

字段说明
DB_TRX_ID事务ID:记录最后一次修改(INSERT/UPDATE)该行的事务ID。
DB_ROLL_PTR回滚指针:指向undo log中的一条记录,该记录包含了该行的旧版本信息。
DB_ROW_ID行ID:如果表没有主键,InnoDB会创建一个隐式的行ID。

📌 关键DB_TRX_IDDB_ROLL_PTR 是MVCC的基石。


🔹 2. Undo Log(回滚日志)

  • 当一条记录被更新时,InnoDB会把旧版本的数据(修改前的值)写入 Undo Log
  • DB_ROLL_PTR 就指向了这条Undo Log记录。
  • 多次更新会形成一条版本链(Version Chain),从最新版本通过DB_ROLL_PTR可以一直追溯到最老的版本。
版本链(从新到旧):
[最新版: balance=300, DB_TRX_ID=102, DB_ROLL_PTR→] --> [旧版: balance=200, DB_TRX_ID=101, DB_ROLL_PTR→] --> [更旧版: balance=100, DB_TRX_ID=100, DB_ROLL_PTR=NULL]

🔹 3. Read View(读视图)

这是MVCC的核心!当一个事务(假设ID为trx_id)执行 普通SELECT(快照读)时,InnoDB会为它创建一个 Read View

Read View 包含了以下关键信息:

信息说明
m_ids创建Read View时,所有活跃的(未提交的)事务ID列表。
min_trx_idm_ids 中的最小值。
max_trx_id创建Read View时,系统应该分配给下一个事务的ID(即当前最大事务ID+1)。
creator_trx_id创建这个Read View的事务自身的ID。

🔄 三、MVCC工作流程:如何判断数据可见性?

当一个事务通过SELECT访问某一行数据时,InnoDB会拿着该行的DB_TRX_ID和当前事务的Read View,按照以下规则判断该行版本是否可见:

🔍 可见性判断算法

  1. 如果 DB_TRX_ID < min_trx_id

    • 说明修改该行的事务在创建Read View之前就已经提交了。
    • ✅ 可见(是“过去”的已提交数据)。
  2. 如果 DB_TRX_ID >= max_trx_id

    • 说明修改该行的事务在创建Read View之后才开始
    • ❌ 不可见(是“未来”的数据)。
  3. 如果 min_trx_id <= DB_TRX_ID < max_trx_id

    • 需要检查 DB_TRX_ID 是否在 m_ids 列表中:
      • 如果在 m_ids 中:说明修改该行的事务在创建Read View时还未提交
      • ❌ 不可见
      • 如果不在 m_ids 中:说明修改该行的事务在创建Read View时已经提交了。
      • ✅ 可见
  4. 如果 DB_TRX_ID == creator_trx_id

    • 说明这行数据就是当前事务自己修改的
    • ✅ 可见(自己做的修改当然要看到)。

📌 简单记忆:一个版本对于当前事务是可见的,当且仅当:

  • 它是由当前事务自身产生的,或者
  • 它是由一个在当前事务创建Read View之前就已经提交的事务产生的。

🧪 四、实战案例:揭秘“非阻塞读”

🎯 场景重现

-- 假设当前系统事务ID分配到 100
-- 账户A: id=1, balance=200, DB_TRX_ID=99 (由事务99插入)

-- 事务1 (ID=101) 启动
START TRANSACTION; -- 创建Read View
-- Read View: m_ids=[], min_trx_id=null, max_trx_id=101, creator_trx_id=101
-- (此时无活跃事务)

-- 事务2 (ID=102) 启动
START TRANSACTION;
UPDATE accounts SET balance = 300 WHERE id = 1; 
-- 生成新版本: balance=300, DB_TRX_ID=102, DB_ROLL_PTR指向旧版本(balance=200)
COMMIT; -- 事务102提交

-- 事务1 继续执行
SELECT balance FROM accounts WHERE id = 1; 
-- 普通SELECT -> 快照读,使用事务1创建的Read View
-- 检查最新版本 (DB_TRX_ID=102):
--   102 >= max_trx_id(101)? 是 -> 规则2 -> ❌ 不可见!
-- 回滚到旧版本 (balance=200, DB_TRX_ID=99):
--   99 < min_trx_id? min_trx_id不存在,视为无穷大 -> 规则1 -> ✅ 可见!
-- 结果:返回 balance=200

-- 事务1 提交
COMMIT;

结果:事务1读到了200,而事务2的更新300对它不可见,实现了可重复读(REPEATABLE READ) 隔离级别。

关键:事务1的SELECT没有阻塞事务2的UPDATE,因为SELECT是快照读,不加锁!


🔍 五、快照读 vs 当前读

MVCC主要影响快照读。还有一种读叫当前读

类型SQL示例是否使用MVCC是否加锁
快照读 (Snapshot Read)SELECT * FROM t;✅ 是❌ 否
当前读 (Current Read)SELECT ... LOCK IN SHARE MODE<br>SELECT ... FOR UPDATE<br>UPDATE ...<br>DELETE ...<br>INSERT ...❌ 否✅ 是(加S锁或X锁)

⚠️ 重要UPDATEDELETE语句虽然包含SELECT,但它们是当前读!它们会读取最新的已提交数据,并加锁,以防止更新丢失。


🎯 总结:MVCC核心要点

概念作用
多版本通过Undo Log保存数据历史版本。
隐藏字段DB_TRX_ID 标记版本创建者,DB_ROLL_PTR 链接版本。
Read View事务的“快照”,定义了可见性规则。
可见性算法基于DB_TRX_ID和Read View判断版本是否可见。
快照读普通SELECT,无锁,读历史版本。
当前读FOR UPDATE等,加锁,读最新版本。
优势读写不冲突,高并发性能。
隔离级别RR(可重复读)和RC(读已提交)都基于MVCC实现。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值