进阶-InnoDB引擎-MVCC

一、MySQL进阶

"当两个用户同时查看同一份数据,你希望看到的是'当前状态',还是'事务开始时的状态'?"

------ MVCC(多版本并发控制),让数据库在高并发下依然保持"一致性"与"高效性"

为什么需要MVCC?------一场"读写冲突"的危机

在传统数据库中,读操作会阻塞写操作,写操作会阻塞读操作。例如:

  • 事务A:UPDATE orders SET status='shipped' WHERE order_id=100
  • 事务B:SELECT * FROM orders WHERE order_id=100

如果事务B在事务A未提交时执行,会发生什么?

  • 悲观锁:事务B必须等待事务A提交(锁表),导致性能下降
  • 乐观锁:事务B直接读取,但可能读到未提交数据(脏读)

💡 2010年,某电商网站因锁表问题导致双11期间响应时间从50ms飙升至500ms,损失超百万!

这就是 MVCC诞生的背景在不加锁的情况下,实现高并发读写

1. InnoDB引擎-MVCC

📌 什么是MVCC?

MVCC(Multi-Version Concurrency Control) 是一种并发控制机制,它通过 保存数据的多个版本 ,让读操作无需等待写操作,从而实现 "读不阻塞写,写不阻塞读"

📌 两种读操作:

类型 说明 举例 是否加锁
当前读(Current Read) 读取最新数据(已提交) SELECT ... FOR UPDATE UPDATE DELETE ✅ 加锁(X锁)
快照读(Snapshot Read) 读取事务开始时的快照 SELECT * ❌ 不加锁

💡 关键点快照读是MVCC的核心,它利用历史版本实现非阻塞读。

MVCC的核心组件:三个隐式字段

InnoDB 通过 三个隐藏字段(在表中不可见)实现MVCC

字段 作用 类型 说明
DB_TRX_ID 记录最近修改该行的事务ID 6字节 用于判断行是否对当前事务可见
DB_ROLL_PTR 指向Undo Log的指针 7字节 用于找到历史版本
DB_ROW_ID 隐藏的行ID(自增) 6字节 在索引中作为回表的唯一标

💡 为什么需要这些字段?

  • DB_TRX_ID:判断行是否已提交
  • DB_ROLL_PTR:通过Undo Log找到旧版本
  • DB_ROW_ID:在索引中唯一标识行(即使主键未指定)

Undo Log:数据的"时间胶囊"

Undo Log 是 MVCC 的基础,它记录了 数据修改前的旧值,用于:

  • 事务回滚
  • MVCC 快照读

📌 Undo Log 两种类型:

类型 作用 说明
INSERT 记录插入的行 用于事务回滚(删除时需恢复)
UPDATE 记录修改前的旧值 用于快照读和回滚(更新时需恢复)

💡 Undo Log 的存储

由 InnoDB 的 Undo Tablespace 管理,可以配置多个 Undo Log 文件(MySQL 5.7+ 支持独立Undo表空间)。

数据版本链:Undo Log 的"链式存储"

当一行数据被多次修改时,Undo Log 会形成 版本链

复制代码
最新版本 → (DB_ROLL_PTR) → 旧版本 → (DB_ROLL_PTR) → 更旧版本 → ... → 初始版本

💡 举例

  • 初始:balance=1000
  • 事务1:UPDATE balance=800 → 生成Undo Log(记录1000)
  • 事务2:UPDATE balance=500 → 生成Undo Log(记录800)

版本链:500 → 800 → 1000

ReadView:MVCC的"时间机器"

ReadView 是事务开始时生成的快照,它决定了哪些版本对当前事务可见。

📌 ReadView 包含的关键信息:

字段 说明
min_trx_id 活跃事务最小ID(当前正在执行的事务ID)
max_trx_id 活跃事务最大ID(当前正在执行的事务ID上限)
m_ids 活跃事务ID列表(当前正在执行的事务ID集合)
python 复制代码
def is_visible(trx_id, readview):
    if trx_id < readview.min_trx_id:  # 已提交
        return True
    elif trx_id >= readview.max_trx_id:  # 未提交
        return False
    else:  # 在活跃事务列表中
        if trx_id in readview.m_ids:  # 未提交
            return False
        else:  # 已提交
            return True

💡 关键点

  • RR级别(可重复读):事务开始时生成ReadView,后续查询都使用同一ReadView
  • RC级别(读已提交):每次查询都生成新的ReadView

MVCC工作原理:一次事务的"时间穿越"

假设执行:

sql 复制代码
-- 事务A(ID=100)
START TRANSACTION;
SELECT * FROM accounts WHERE user_id=1;  -- 快照读
UPDATE accounts SET balance=500 WHERE user_id=1;  -- 当前读
COMMIT;

-- 事务B(ID=101)
START TRANSACTION;
SELECT * FROM accounts WHERE user_id=1;  -- 快照读

📌 MVCC如何工作?

  1. 事务A开始
    • 生成ReadView:min_trx_id=100, max_trx_id=102, m_ids=[100]
    • 执行SELECT:快照读,使用ReadView判断可见性
  2. 事务A执行UPDATE
    • 修改行,生成Undo Log(记录旧值:1000 → 500)
    • DB_TRX_ID 更新为100
    • DB_ROLL_PTR 指向Undo Log
  3. 事务B执行SELECT
    • 生成ReadView:min_trx_id=101, max_trx_id=102, m_ids=[101]
    • 读取行:DB_TRX_ID=100 < 101 → 可见(值为1000)

🌟 效果:事务B在事务A未提交时,读到的是1000(事务A开始时的值),而非500(当前值)。

MVCC在不同隔离级别下的表现

隔离级别 快照读 可见性规则 幻读 说明
读未提交 不使用MVCC 直接读最新数据 无MVCC
读已提交 使用MVCC 每次查询生成新ReadView 无法避免不可重复读
可重复读 使用MVCC 事务开始时生成ReadView 通过Next-Key Lock避免幻读
串行化 不使用MVCC 强制加锁 无MVCC

💡 为什么RR级别能避免幻读?

InnoDB 在RR级别下使用 Next-Key Lock(记录锁 + 间隙锁),在读取时不仅锁住记录,还锁住间隙,防止其他事务插入新数据。

相关推荐
Elastic 中国社区官方博客1 小时前
更高的吞吐量和更低的延迟: Elastic Cloud Serverless 在 AWS 上获得了显著的性能提升
大数据·数据库·elasticsearch·搜索引擎·云原生·serverless·aws
坚持学习前端日记1 小时前
认证模块文档
java·服务器·前端·数据库·spring
想摆烂的不会研究的研究生1 小时前
并发场景——实时排行榜设计
数据库·redis·后端·缓存
2501_9481953410 小时前
RN for OpenHarmony英雄联盟助手App实战:主导航实现
数据库
Filotimo_10 小时前
N+1查询问题
数据库·oracle
fenglllle11 小时前
spring-data-jpa saveall慢的原因
数据库·spring·hibernate
DarkAthena12 小时前
【GaussDB】执行索引跳扫时如果遇到该索引正在执行autovacuum,可能会导致数据查询不到
数据库·gaussdb
短剑重铸之日12 小时前
《7天学会Redis》Day 5 - Redis Cluster集群架构
数据库·redis·后端·缓存·架构·cluster