MySQL 是如何实现事务隔离的?

1. 引言

在数据库系统中,事务隔离是保证并发环境下数据一致性的核心机制。MySQL 作为最流行的关系型数据库之一,其 InnoDB 存储引擎通过一套精妙的设计实现了 SQL 标准中定义的四种隔离级别。本文将深入剖析 MySQL 底层是如何实现事务隔离的,帮助读者真正理解 MVCC、锁机制与隔离级别之间的关系。

2. 事务隔离级别概述

SQL 标准定义了四种事务隔离级别,分别对应不同的并发问题容忍度:

隔离级别 脏读 不可重复读 幻读
READ UNCOMMITTED(读未提交) 可能 可能 可能
READ COMMITTED(读已提交) 不可能 可能 可能
REPEATABLE READ(可重复读) 不可能 不可能 可能(InnoDB 已解决)
SERIALIZABLE(串行化) 不可能 不可能 不可能

MySQL InnoDB 默认使用 REPEATABLE READ(可重复读) 隔离级别,但通过 MVCC 和间隙锁的配合,实际上已经解决了幻读问题,达到了接近 SERIALIZABLE 的效果。

3. 核心机制一:MVCC(多版本并发控制)

MVCC 是 MySQL 实现事务隔离的基石。它的核心思想是:读操作不阻塞写操作,写操作不阻塞读操作,通过保存数据的历史版本来实现。

3.1 隐藏列

InnoDB 的每行记录除了用户定义的字段外,还包含三个隐藏列:

  • DB_TRX_ID:最近一次修改该行的事务 ID
  • DB_ROLL_PTR:回滚指针,指向 undo log 中的旧版本记录
  • DB_ROW_ID:隐藏主键(当表没有主键时自动生成)

3.2 Undo Log 版本链

每次事务修改数据时,InnoDB 会先将旧值写入 undo log,然后通过回滚指针将新旧版本串联成一条版本链。这条链上的每个版本都记录了对应的事务 ID,供后续的可见性判断使用。

3.3 ReadView(读视图)

ReadView 是 MVCC 实现快照读的关键数据结构,它记录了事务启动时活跃事务的 ID 列表,主要包含:

  • m_ids:生成 ReadView 时当前活跃(未提交)的事务 ID 列表
  • min_trx_id:活跃事务中最小的 ID
  • max_trx_id:下一个将被分配的事务 ID
  • creator_trx_id:创建该 ReadView 的事务 ID

3.4 可见性判断规则

当执行快照读时,沿着版本链从新到旧逐个判断版本的可见性:

  1. 若版本的事务 ID 等于 creator_trx_id,则可见(自己修改的)
  2. 若版本的事务 ID 小于 min_trx_id,则可见(已提交)
  3. 若版本的事务 ID 大于等于 max_trx_id,则不可见(未来事务)
  4. 若版本的事务 ID 在 m_ids 中,则不可见(活跃事务)
  5. 否则可见(已提交)
sql 复制代码
-- 示例:查看当前事务隔离级别
SELECT @@transaction_isolation;

-- 示例:开启一个事务并查询
START TRANSACTION;
SELECT * FROM user WHERE id = 1;
COMMIT;

4. 核心机制二:锁机制

MVCC 解决了快照读的隔离问题,但对于当前读(如 SELECT ... FOR UPDATEUPDATEDELETE)以及写操作之间的冲突,则需要依赖锁机制。

4.1 锁的分类

InnoDB 的锁主要分为以下几类:

  • 共享锁(S Lock):允许其他事务读取,但阻止写入
  • 排他锁(X Lock):阻止其他事务读取和写入
  • 记录锁(Record Lock):锁定单条索引记录
  • 间隙锁(Gap Lock):锁定一个范围,但不包含记录本身
  • 临键锁(Next-Key Lock):记录锁 + 间隙锁的组合,锁定范围及范围内的记录

4.2 当前读与快照读

  • 快照读 :普通的 SELECT 语句,使用 MVCC 读取历史版本,不加锁
  • 当前读SELECT ... FOR UPDATEUPDATEDELETE 等,读取最新版本并加锁
sql 复制代码
-- 当前读示例:加排他锁
SELECT * FROM user WHERE id = 1 FOR UPDATE;

-- 当前读示例:加共享锁
SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE;

5. 各隔离级别的实现原理

5.1 READ UNCOMMITTED

  • 读操作不加锁,直接读取最新版本
  • 不生成 ReadView,因此可能读到未提交的数据(脏读)
  • 实际使用场景极少

5.2 READ COMMITTED

  • 每次执行快照读时都生成一个新的 ReadView
  • 因此每次 SELECT 都能看到最新已提交的数据
  • 解决了脏读,但无法避免不可重复读
sql 复制代码
-- 会话 A
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT * FROM user WHERE id = 1;  -- 读到 value = 100

-- 会话 B 此时提交 UPDATE user SET value = 200 WHERE id = 1;

SELECT * FROM user WHERE id = 1;  -- 再次读到 value = 200(不可重复读)
COMMIT;

5.3 REPEATABLE READ(InnoDB 默认)

  • 事务第一次执行快照读时生成 ReadView,后续复用同一个 ReadView
  • 因此整个事务期间看到的数据快照是一致的,解决了不可重复读
  • 通过间隙锁临键锁解决幻读问题
sql 复制代码
-- 会话 A
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT * FROM user WHERE id = 1;  -- 读到 value = 100

-- 会话 B 此时提交 UPDATE user SET value = 200 WHERE id = 1;

SELECT * FROM user WHERE id = 1;  -- 仍然读到 value = 100(可重复读)
COMMIT;

5.4 SERIALIZABLE

  • 所有读操作都自动加共享锁,写操作加排他锁
  • 通过强制串行执行来保证最高的隔离性
  • 并发性能最低,实际使用较少

6. 幻读的解决:间隙锁与临键锁

在 REPEATABLE READ 级别下,InnoDB 通过临键锁(Next-Key Lock) 来解决幻读问题。临键锁是记录锁和间隙锁的组合,它锁定的不仅是指定记录,还包括该记录之前的间隙。

sql 复制代码
-- 假设 user 表 id 有 1、5、10 三条记录
-- 执行以下当前读:
SELECT * FROM user WHERE id BETWEEN 1 AND 10 FOR UPDATE;

-- InnoDB 会锁定:
-- 间隙 (1, 5)、记录 5、间隙 (5, 10)、记录 10、间隙 (10, +∞)
-- 其他事务无法在 (1, 10] 范围内插入新记录,从而防止幻读

7. 总结

MySQL InnoDB 实现事务隔离的核心可以概括为:

  • MVCC 负责快照读:通过隐藏列、undo log 版本链和 ReadView 实现非锁定读,让读操作不阻塞写操作
  • 锁机制负责当前读与写冲突:通过记录锁、间隙锁和临键锁保证写操作的隔离性
  • 隔离级别决定 ReadView 的生成时机:READ COMMITTED 每次生成新 ReadView,REPEATABLE READ 复用首次生成的 ReadView
  • REPEATABLE READ + 临键锁:让 InnoDB 在默认隔离级别下就解决了幻读问题

理解这些底层机制,不仅能帮助我们正确设置隔离级别,还能在排查死锁、优化并发性能时做到心中有数。

相关推荐
螺蛳粉 螺蛳粉1 小时前
mysql备份脚本
mysql
IT大白鼠1 小时前
MySQL 高可用系列 · Orchestrator + ProxySQL 第三篇——核心原理精讲
数据库·mysql·架构
国际云,接待3 小时前
云服务器跨厂商迁移怎么尽量不停机:用 rsync、MySQL binlog 和 DNS TTL 做双阶段切换
运维·服务器·mysql
小白学大数据4 小时前
Python 项目实战:用 Flask 构建生产级 MySQL 增删改查 REST API
开发语言·人工智能·python·mysql·flask
minebmw74 小时前
第 02 讲 基础架构:一条 SQL 查询语句是如何执行的?
mysql
jaysee-sjc4 小时前
【苍穹外卖】Day01:从零认识企业级项目开发
java·开发语言·数据库·mysql·spring·intellij-idea·mybatis
实心儿儿5 小时前
MySQL — 库的操作
数据库·mysql·oracle
bksczm5 小时前
MySQL基础篇之索引
数据库·sql·mysql
bksczm6 小时前
MySQL基础篇之事务
linux·数据库·sql·mysql