MySQL锁与MVCC深度解析

前言

在MySQL InnoDB引擎中,事务ACID特性是数据库可靠存储的基石。redo log保证事务持久性,undo log实现事务原子性,锁机制解决写写冲突MVCC多版本并发控制实现读写不阻塞。这四者相互配合,在保证数据一致性的前提下支撑高并发业务。很多面试中锁、MVCC、事务隔离级别问题,本质都是考察这几个组件如何协同工作。

java 复制代码
ACID
├─ A 原子性 → undo log
├─ D 持久性 → redo log
└─ I 隔离性 → 锁 + MVCC
C 一致性:原子、隔离、持久共同保障的最终结果

一、事务ACID四大特性

事务是一组不可拆分的SQL操作集合,要么全部执行成功,要么全部失败回滚。ACID是事务必须满足的四个特性:

A --- Atomicity 原子性

事务是最小执行单元,事务内所有操作,要么全部成功,要么全部回滚。

  • 底层实现:undo log(回滚日志)
  • 原理:修改数据前,会把数据旧镜像写入undo log。事务失败回滚时,利用undo log还原数据;事务提交后,undo log不会立刻删除,由Purge线程清理不再被MVCC引用的版本。

C --- Consistency 一致性

事务执行前后,数据库状态始终保持合法。不会出现违反数据约束的非法数据。

一致性是最终目标 ,不是由单一组件实现。原子性、隔离性、持久性三者共同保证一致性。

约束包括:主键唯一、外键约束、字段非空、业务层面数据平衡(例如转账前后账户总金额不变)。

I --- Isolation 隔离性

多个并发事务之间相互隔离,防止并发事务带来的数据异常。隔离级别定义并发事务之间数据可见性规则。并发会产生三类经典问题:脏读、不可重复读、幻读。

隔离级别 脏读 不可重复读 幻读
Read Uncommitted(读未提交) ✅存在 ✅存在 ✅存在
Read Committed(读已提交 RC) ❌不存在 ✅存在 ✅存在
Repeatable Read(可重复读 RR,MySQL默认) ❌不存在 ❌不存在 快照读无幻读;当前读依靠临键锁防止幻读
Serializable(串行化) ❌不存在 ❌不存在 ❌不存在

隔离级别越高,并发性能越低;隔离级别越低,并发越高,但数据异常风险越大。

D --- Durability 持久性

事务一旦提交成功,修改永久落盘,即使数据库宕机、断电,数据也不会丢失。

  • 底层实现:redo log(重做日志)
  • 原理:事务提交时优先写入redo log,内存中的脏页不会立刻刷磁盘。数据库崩溃重启后,通过redo log重放已经提交事务,恢复数据。

📌ACID组件关系图

二、redo log 与 undo log 概念以及区别

redo log(重做日志)

核心作用:保障事务持久性D

  • 记录内容:修改之后的数据页内容,属于物理日志。
  • 写入时机:事务执行过程不断写入redo log buffer;事务提交时刷入磁盘redo log文件。
  • 使用场景:数据库宕机,内存脏页没有刷到磁盘,重启数据库,读取redo log重放已经提交事务。
  • 相关参数:innodb_log_file_sizeinnodb_flush_log_at_trx_commit

📌redo log 刷盘流程|WAL预写日志流程

undo log(回滚日志)

核心作用:保障事务原子性A;同时为MVCC提供历史数据版本

  • 记录内容:修改之前的旧数据,属于逻辑日志。update记录旧值,delete记录反向insert操作。
  • 写入时机:修改数据之前写入undo log。
  • 使用场景:
    1. 事务回滚,利用undo log还原旧数据,保证原子性;
    2. MVCC快照读,沿着版本链读取历史版本。
  • 分类:
    • insert_undo:insert语句产生,事务提交直接删除,不参与MVCC;
    • update_undo:update/delete产生,用于MVCC,由Purge线程回收。

📌undo log 版本链示意图

redo log vs undo log对比表

对比维度 redo log undo log
核心职责 持久性D 原子性A + MVCC多版本
记录内容 修改之后的数据页 修改之前的旧数据
日志类型 物理日志 逻辑日志
核心作用 崩溃恢复,恢复已提交修改 事务回滚、提供历史快照版本
生命周期 崩溃恢复完成即可丢弃 事务提交后,若仍被MVCC引用,则保留,Purge线程清理
写入时机 事务执行中,提交刷盘 修改数据之前写入
解决问题 宕机丢失已提交事务 事务回滚、快照读

补充:binlog属于MySQL Server层日志,用于主从复制、数据备份,不属于ACID底层保障日志。

三、MySQL InnoDB锁机制全解

InnoDB锁基于索引实现,如果没有命中索引,行锁会退化成表锁。所有锁在事务提交或者回滚之后释放。

📌锁的分类结构图

3.1 共享锁S & 排他锁X

  • S共享锁(读锁)SELECT ... LOCK IN SHARE MODE。多个事务可以同时加S锁;S锁和X锁互斥。
  • X排他锁(写锁) :UPDATE、DELETE、INSERT、SELECT ... FOR UPDATE。X锁和所有锁互斥。

3.2 表级锁

  1. 意向锁 IS / IX:表级锁,InnoDB自动添加。作用:高效判断行锁与表锁之间冲突。加行S锁前先加IS锁;加行X锁前先加IX锁。意向锁之间互相兼容。
  2. MDL元数据锁:保护表结构。DML语句获取MDL读锁(读锁之间兼容);DDL语句获取MDL写锁(写锁互斥)。长事务持有MDL读锁,会阻塞DDL,进而阻塞后续所有DML,是线上常见故障点。
  3. LOCK TABLES:手动表锁,生产环境禁止使用。

3.3 行级锁(RR隔离级别下生效)

  1. 记录锁 Record Lock:锁定索引上某一条具体记录。
  2. 间隙锁 Gap Lock:锁定索引记录之间的间隙,不锁定记录本身。RC隔离级别没有间隙锁;间隙锁之间互相兼容,主要防止其他事务在间隙插入数据。
  3. 临键锁 Next‑Key Lock = 记录锁 + 间隙锁,左开右闭区间,RR默认行锁算法,用来解决当前读幻读
    • 唯一索引等值查询命中记录 → 临键锁退化为记录锁;
    • 唯一索引等值查询找不到记录 → 退化为间隙锁。

📌临键锁区间示意图

3.4 特殊锁

  • 插入意向锁:INSERT操作时在间隙上加的锁。多个事务向同一个间隙的不同位置插入数据互不阻塞。
  • AUTO‑INC锁 :自增主键ID分配锁,通过innodb_autoinc_lock_mode控制并发策略。

锁兼容性矩阵

锁类型 X锁 IX锁 S锁 IS锁
X锁 互斥 互斥 互斥 互斥
IX锁 互斥 兼容 互斥 兼容
S锁 互斥 互斥 兼容 兼容
IS锁 互斥 兼容 兼容 兼容

死锁

  • 死锁产生4个必要条件:互斥条件、占有且等待、不可剥夺、循环等待。破坏任意一条即可避免死锁。
  • 常见场景:事务交叉顺序更新数据、范围查询触发临键锁、无索引更新退化为表锁。
  • 规避方案:固定事务加锁顺序、SQL必须走索引、避免长事务、RC隔离级别减少间隙锁。

四、MVCC多版本并发控制

MVCC全称Multi‑Version Concurrency Control,多版本并发控制。实现快照读不加锁,读写不阻塞,RC、RR隔离级别生效。

4.1 MVCC三大核心组件

  1. 隐藏列(每行数据自带)
    • DB_TRX_ID:6字节,最近修改该行记录的事务ID;
    • DB_ROLL_PTR:7字节,回滚指针,指向undo log里旧版本数据;
    • DB_ROW_ID:6字节,没有主键时的隐藏主键,非MVCC核心。
  2. undo log:存放旧版本数据,串联成版本链。
  3. 版本链 :多次修改同一行数据时,DB_ROLL_PTR不断指向undo log历史版本,形成版本链表,链表头部是最新数据。

4.2 ReadView 可见性视图

ReadView是事务查询时生成的快照,用来判断当前事务可以看到哪个版本的数据,包含4个字段:

  • m_ids:创建ReadView时刻,所有未提交活跃事务ID集合;
  • min_trx_id:m_ids集合里面最小事务ID;
  • max_trx_id:创建ReadView时,系统将要分配的下一个事务ID;
  • creator_trx_id:创建ReadView的当前事务ID。

📌ReadView可见性判断流程图

可见性判断逻辑

  1. row_trx_id == creator_trx_id:数据是当前事务自己修改的,可见;
  2. row_trx_id < min_trx_id:修改该数据的事务已经提交,可见;
  3. row_trx_id >= max_trx_id:这个版本是 ReadView 创建之后才产生,不可见;
  4. row_trx_id 在 m_ids集合中:事务还未提交,不可见;否则可见。

4.3 RC 与 RR 隔离级别 ReadView 差异

  • RC(读已提交)每一条 SELECT 语句都会新建 ReadView。每次查询都会读取最新提交的数据,产生不可重复读。
  • RR(可重复读)事务第一条 SELECT 创建 ReadView,后续 SELECT 复用同一个 ReadView。同一个事务多次查询看到相同快照,实现可重复读。

4.4 快照读 vs 当前读

  • 快照读 :普通SELECT语句,不加锁,读取版本链历史数据。MVCC 的应用场景。
  • 当前读 :UPDATE、DELETE、INSERT、SELECT ... FOR UPDATESELECT ... LOCK IN SHARE MODE,加锁读取最新版本数据,依赖锁机制保证隔离。

RR 隔离级别:快照读依靠 MVCC 屏蔽幻读;当前读依靠临键锁锁住索引间隙,阻止插入新数据,避免幻读。

4.5 Purge 线程

Purge 线程负责清理不再被任何 ReadView 引用的 undo log 历史版本。长事务会持续持有 ReadView,阻止 Purge 清理 undo log,造成 undo log 膨胀,磁盘占用持续上涨,数据库性能下降

五、SQL 演示:脏读、不可重复读、幻读

准备测试表,新建两个 MySQL 会话:T1、T2。

SQL 复制代码
CREATE TABLE test_account (
  id INT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(20),
  money INT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO test_account(name,money) VALUES ('A',100),('B',200);

实验 1:脏读(Read Uncommitted 读未提交)

脏读:一个事务读到另一个事务未提交的数据。

SQL 复制代码
-- T1会话
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;

-- T2会话
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
UPDATE test_account SET money = 999 WHERE id=1; -- T2修改,但不提交

-- T1查询,可以读到T2未提交的money=999,发生脏读
SELECT * FROM test_account;

-- T2执行回滚
ROLLBACK;

RC 及以上隔离级别,不会出现脏读。

实验 2:不可重复读(RC 读已提交)

不可重复读:同一个事务内,两次读取同一行记录,中间被其他事务修改并提交,两次查询结果不一致。

SQL 复制代码
-- T1会话
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT * FROM test_account WHERE id=1; -- 第一次读取 money=100

-- T2会话
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
UPDATE test_account SET money=666 WHERE id=1;
COMMIT; -- T2提交修改

-- T1再次查询,读到money=666,同一事务两次结果不一致,不可重复读
SELECT * FROM test_account WHERE id=1;

RR 隔离级别不会出现不可重复读,因为事务复用同一个 ReadView。

实验 3:幻读演示

幻读:同一事务,相同范围查询,前后查询行数不一致,其他事务插入 / 删除满足条件的数据。

场景:RC 隔离级别,当前读,出现幻读

SQL 复制代码
-- T1会话
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT * FROM test_account WHERE id>1 FOR UPDATE; -- 当前读

-- T2会话
START TRANSACTION;
INSERT INTO test_account(name,money) VALUES ('C',300);
COMMIT;

-- T1再次当前读,可以读到T2新插入的数据,出现幻读
SELECT * FROM test_account WHERE id>1 FOR UPDATE;

RR 隔离级别下执行同样select ... for update会触发临键锁,锁住索引间隙,T2 无法执行 INSERT,不会出现当前读幻读。

六、总结与生产实践建议

  1. ACID:原子性由 undo log 保障;持久性由 redo log 保障;隔离性依靠锁 + MVCC;一致性是最终目标。
  2. redo log 保存修改后数据页,用于崩溃恢复;undo log 保存旧数据,用于事务回滚和 MVCC 版本链。
  3. MVCC 依靠隐藏字段 + undo 版本链 + ReadView 实现快照读无锁;RC 每次查询新建 ReadView,RR 事务第一次 select 创建并复用 ReadView。
  4. 锁解决写写冲突;MVCC 解决读写冲突。RR 隔离级别:快照读依靠 MVCC 防止幻读,当前读依靠临键锁防止幻读。
  5. 生产实践建议
    • 高并发业务可以考虑 RC 隔离级别,无间隙锁,并发性能更好;但 RC 会存在不可重复读、当前读幻读,业务层需要兼容;
    • DML 语句务必保证命中索引,否则行锁退化为表锁;
    • 禁止长事务,长事务会引发 MDL 阻塞、undo log 膨胀;
    • DDL 操作避开业务高峰,防止 MDL 锁阻塞线上业务;
    • 业务尽量避免大范围范围更新,防止临键锁锁定大量索引区间,引发锁等待、死锁。
相关推荐
底层玩家老张1 小时前
什么是关系型数据库:一次库存超卖事故的内核复盘
高并发·mvcc·关系型数据库·存储引擎·acid
数据库小学妹2 小时前
MySQL库存扣减实战:原子UPDATE、分桶方案与锁范围分析
数据库·后端·mysql
Linux-lucky3 小时前
39-41-Linux学习之旅之redis缓存基础与NFS基础
linux·运维·mysql·ubuntu
于平安3 小时前
MySQL-变量,流程控制与游标
数据库·mysql
徐子童4 小时前
介绍MVCC机制
java·mysql·面试题·秋招·并发·mvcc
thefool1122664 小时前
库和表基本操作、表的增删改查
mysql
Rain的Java大神之路15 小时前
如何快速上传10G文件
java·spring boot·redis·后端·mysql·spring cloud·面试
大海星辰79817 小时前
一次对账SQL“灵异事件”排查:NOT IN返回空集的坑
mysql