MYSQL三大日志、隔离级别(MVCC+锁机制实现)

MySQL三大日志

​Undo Log(回滚日志)

作用

事务回滚时恢复数据到修改前的状态。

支持 ​​MVCC​​,为读操作提供历史版本数据。

存储

存放在 undo tablespace 中,通过回滚段管理。

格式

undo log 格式都有一个 roll_pointer 指针和一个 trx_id 事务id:

通过 trx_id 可以知道该记录是被哪个事务修改的;

通过 roll_pointer 指针可以将这些 undo log 串成一个链表,这个链表就被称为版本链;

​​Redo Log(重做日志)

作用

保证事务的​​持久性​​,记录物理页的修改(物理日志,比如对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新,每当执行一个事务就会产生这样的一条或者多条物理日志),用于崩溃恢复。

写入时机​​

事务执行中按顺序循环写入,通过 innodb_flush_log_at_trx_commit 控制刷盘策略(0/1/2)。

组成

内存中的 redo log buffer 和磁盘上的 redo log file。

undo log 和 redo log 区别

undo log 记录了此次事务「开始前」的数据状态,记录的是更新之前的值;

redo log 记录了此次事务「完成后」的数据状态,记录的是更新之后的值;

事务提交之前发生了崩溃,重启后会通过 undo log 回滚事务,事务提交之后发生了崩溃,重启后会通过 redo log 恢复事务

​​​​Binlog(二进制日志)

MySQL 在完成一条更新操作后,Server 层还会生成一条 binlog,等之后事务提交的时候,会将该事物执行过程中产生的所有 binlog 统一写 入 binlog 文件。

作用

记录所有对数据库的​​修改操作​​(逻辑日志),用于主从复制、数据恢复(如通过 mysqlbinlog 工具恢复数据)。

写入时机​​

事务提交后顺序写入,通过参数 sync_binlog 控制刷盘策略。

格式

STATEMENT(记录SQL语句)、ROW(记录行数据变化)、MIXED(混合模式)。

redo log 和 binlog 区别

1、适用对象不同:

binlog 是 MySQL 的 Server 层实现的日志,所有存储引擎都可以使用;

redo log 是 Innodb 存储引擎实现的日志;

2、文件格式不同:

binlog 记录每一条修改数据的 SQL (相当于记录了逻辑操作,所以针对这种格式, binlog 可以称为逻辑日志),

redo log 是物理日志,记录的是在某个数据页做了什么修改,比如对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新;

3、写入方式不同:

binlog 是追加写,写满一个文件,就创建一个新的文件继续写,不会覆盖以前的日志,保存的是全量的日志。

redo log 是循环写,日志空间大小是固定,全部写满就从头开始,保存未被刷入磁盘的脏页日志。

4、用途不同:

binlog 用于备份恢复、主从复制;

redo log 用于掉电等故障恢复。

MySQL Buffer Pool

核心概念​

MySQL 的数据都是存在磁盘中的,那么我们要更新一条记录的时候,得先要从磁盘读取该记录,然后在内存中修改这条记录。修改完这条记录不是直接写回到磁盘,而是缓存起来。

为此,Innodb 存储引擎设计了一个缓冲池(Buffer Pool),来提高数据库的读写性能。

Buffer Pool​​ 是 InnoDB 引擎的关键内存区域,用于缓存​​数据页和索引页​​,减少磁盘 I/O,提升数据库性能。

所有读写操作首先作用于 Buffer Pool,修改后的页称为​​脏页(Dirty Page)​​,最终由后台线程刷回磁盘。

当读取数据时,如果数据存在于 Buffer Pool 中,客户端就会直接读取 Buffer Pool 中的数据,否则再去磁盘中读取。

当修改数据时,如果数据存在于 Buffer Pool 中,那直接修改 Buffer Pool 中数据所在的页,然后将其页设置为脏页(该页的内存数据和磁盘上的数据已经不一致),为了减少磁盘I/O,不会立即将脏页写入磁盘,后续由后台线程选择一个合适的时机将脏页写入到磁盘。

Buffer Pool 配置​

大小设置​​:

通过参数 innodb_buffer_pool_size 配置,通常设为系统内存的 ​​50%-70%​​。

多实例​​:

在高并发场景下,可通过 innodb_buffer_pool_instances 分割为多个实例,减少锁竞争。

内存管理与 LRU 算法​

LRU 链表优化​​:

InnoDB 将 LRU 链表分为 ​​Young 区(热点数据)​​和 ​​Old 区(新加载数据)​​,避免全表扫描冲刷热点数据。

​​Old SubList​​:新数据页首先插入 Old 区头部。

​​Young SubList​​:若 Old 区的页在 innodb_old_blocks_time(默认 1 秒)后被再次访问,则移到 Young 区。

预读机制​​:

​​线性预读(Linear Read-ahead)​​:基于顺序访问模式,预读相邻页。

​​随机预读(Random Read-ahead)​​:根据页的访问模式预测加载其他页(默认关闭)。

脏页刷新与持久化​

刷新机制​​:

​​Checkpoint​​:定期将脏页刷盘,崩溃恢复时从 Checkpoint 位置开始重放 redo log。

​​自适应刷新(Adaptive Flushing)​​:根据负载动态调整刷盘速率,避免 I/O 尖峰。

​​刷盘触发条件​​:

Buffer Pool 空间不足时。

Redo Log 空间不足(通过日志推进检查点)。

后台线程定期刷新。

与日志系统的协作​

Redo Log​​:事务提交时先写 redo log,确保即使脏页未刷盘,崩溃后仍可通过 redo log 恢复。

​​Undo Log​​:用于事务回滚和 MVCC,其历史版本数据可能存储在 Buffer Pool 的 undo 页中。

事务隔离级别​

MySQL 支持四种隔离级别,通过 ​​MVCC​​ 和​​锁机制​​实现:

READ UNCOMMITTED(读未提交)​​

事务可以读到其他未提交事务的修改。

​​问题​​:脏读、不可重复读、幻读。

READ COMMITTED(读已提交,RC)​​

事务只能读到其他已提交事务的修改。

​​实现​​:每次 SELECT 生成新的 ReadView。

​​问题​​:不可重复读、幻读。

REPEATABLE READ(可重复读,RR)​​

​​默认隔离级别​​,事务内多次读取同一数据的结果一致。

​​实现​​:事务第一次 SELECT 时生成 ReadView,后续复用。

​​解决​​:脏读、不可重复读;通过​​间隙锁(Gap Lock)​​ 避免幻读(当前读时生效)。

SERIALIZABLE(串行化)​​

所有操作加锁,强制事务串行执行。

​​解决​​:所有并发问题,但性能最低。

MVCC

MVCC(多版本并发控制 Multi-Version Concurrency Control)是数据库实现高并发访问的核心机制,​​通过维护数据的多个版本,允许读操作不阻塞写操作,写操作也不阻塞读操作​​。InnoDB 引擎使用 MVCC 实现了 ​​非阻塞读​​,显著提升了数据库的并发性能,尤其在高读低写的场景下效果显著。

MVCC 的核心原理​

​​隐藏字段​​

InnoDB 的每行数据包含两个隐藏字段:

​​DB_TRX_ID​​:最近修改该行数据的事务 ID(事务提交时写入)。

​​DB_ROLL_PTR​​:指向 Undo Log 的回滚指针,用于构建数据的历史版本链。

​​Undo Log​​

存储数据的历史版本,通过回滚指针(DB_ROLL_PTR)串联成一个链表(版本链)。

​​作用​​:

事务回滚时恢复数据到旧版本。

提供 MVCC 所需的历史版本数据。

ReadView​​(读视图)

事务在发起读操作时生成的一个​​快照​​,用于判断数据版本的可见性。

​​关键信息​​:

m_ids:生成 ReadView 时活跃(未提交)的事务 ID 集合。

min_trx_id:活跃事务中的最小事务 ID。

max_trx_id:生成 ReadView 时系统将分配给下一个事务的 ID。

creator_trx_id:创建该 ReadView 的事务 ID(仅当该事务自身有修改时存在)。

数据可见性规则​

MVCC 通过以下规则判断数据版本是否对当前事务可见:

如果数据版本的 ​​DB_TRX_ID < min_trx_id​​,说明该版本在 ReadView 生成前已提交,​​可见​​。

如果数据版本的 ​​DB_TRX_ID >= max_trx_id​​,说明该版本由未来事务修改,​​不可见​​。

如果 ​​DB_TRX_ID 在 m_ids 中​​,说明该版本由未提交的事务修改,​​不可见​​。

如果 ​​DB_TRX_ID 不在 m_ids 中且 DB_TRX_ID < max_trx_id​​,说明该版本已提交,​​可见​​。

特殊处理​​:

如果该版本由当前事务自身修改(DB_TRX_ID == creator_trx_id),则可见。

MVCC 与隔离级别​

不同隔离级别下,MVCC 的行为差异主要体现在 ​​ReadView 的生成时机​​:

READ COMMITTED(读已提交,RC)​​

​​每次 SELECT 都生成新的 ReadView​​。

​​效果​​:能读到其他事务已提交的最新数据,但可能导致不可重复读。

REPEATABLE READ(可重复读,RR)​​

​​事务第一次 SELECT 时生成 ReadView,后续复用​​。(每开启一个事物才会生成一个 readView,一个事务的所有SQL语句共享一个 readView。)

​​效果​​:整个事务中看到的数据版本一致,解决不可重复读。

MVCC 如何解决并发问题​

脏读​​

不可见未提交事务的版本(通过 ReadView 过滤活跃事务的修改)。

例:

如下所示是 student 学生表中的一条数据

(1) 假设现在有事务A和事务B两个事务并发操作该行数据。事务A执行了两次查询操作,事务B执行了一次更新操作;

(2)事务A执行第一次查询操作,先生成 readView ,我们姑且称之为 readView_1,还未开始查询操作,事务B率先执行了更新操作,将数据进行了修改并提交,事务B结束,此时事务A第一次查询开始,但由于事务A已经生成了 readView_1 ,所以它不会读取到事务B修改过后的数据,读取到的是 readView_1 中事务B修改之前的数据,解决了脏读的问题;

(3)然后,事务A进行第二次查询操作。注意!!!这里它又生成了一个 readView,我们称之为 readView_2,此时的 readView_2 中的数据是已经被事务B修改后的数据了,事务A再次进行查询,发现查询到的数据和刚操第一次查询到的不一样了,就产生了不可重复读的问题。

(4)所以说,MVCC 在读已提交隔离级别下只解决了脏读的问题,没有解决不可重复读的问题。

​​不可重复读​​

RR 隔离级别下,复用同一个 ReadView 保证多次读取结果一致。

例:

仍以上面的学生表举例,如下所示

(1)假设现在事务A与事务B并发操作来查询 student 表。事务A 执行SELECT查询操作,执行查询操作之前会生成一个 readView,我们姑且称之为 readView_1 ,事务A从始至终使用的都是 readView_1;

(2)此时事务B来修改 student 数据,可重复读隔离级别中一个事务生成一个 readView ,所以事务B也生成了一个 readView ,我们称之为 readView_2,然后事务B率先修改完毕并提交;

(3)事务A在事务B提交之后才进行的查询,按道理来说因为事务B修改了数据,我们会产生不可重复读,但是因为事务A从始至终都是用的 readView_1 ,所以 事务A在进行查询操作的时候,查询到的其实还是事务B修改之前的数据,由此就解决了不可重复读;而且即便事务A后续进行了多次 SELECT 查询操作,仍然使用最开始生成的 readView,解决了 不可重复读的问题。

(4)总结:其实归根结底,读已提交,可重复读两种隔离级别最关键的因素就是 readView 生成的时机不同,造就了它们不同的隔离级别。

幻读​​

幻读(Phantom Read)是数据库事务隔离性问题的一种,表现为:​​同一事务中多次执行相同范围的查询,后续查询返回了其他事务插入的新行​​。MySQL 在 ​​可重复读(RR)​​ 隔离级别下,通过 ​​MVCC​​ 和 ​​间隙锁(Gap Lock)​​ 结合的方式解决幻读问题。以下是详细分析:

幻读的本质​

​​场景示例​​:

事务 A 查询 age > 20 的用户,返回 10 条数据。

事务 B 插入一条 age = 25 的新用户并提交。

事务 A 再次查询 age > 20,发现返回了 11 条数据(多出事务 B 插入的行)。

​​核心问题​​:​​范围查询中新增的数据破坏了事务的一致性视图​​。

MySQL 解决幻读的机制​
MVCC(快照读)​​

​​原理​​:

在 RR 隔离级别下,事务首次读取数据时生成 ​​ReadView​​,后续查询复用该快照,确保多次读取同一范围的数据时,结果一致(基于历史版本)。

​​效果​​:

​​快照读(普通 SELECT)​​:通过 MVCC 读取历史版本数据,​​不会看到其他事务插入的新行​​,避免了幻读。

​​局限性​​:若事务中执行写操作(如 UPDATE/DELETE),可能触发当前读,导致幻读风险。

间隙锁(Gap Lock)​

​​​​原理​​:

对索引记录之间的"间隙"加锁,阻止其他事务在范围内插入新数据。

​​间隙范围​​:例如,表中现有 id=5 和 id=10 的记录,间隙锁会锁定 (5, 10) 区间。

​​加锁场景​​:

执行 SELECT ... FOR UPDATE(当前读)。

执行 UPDATE 或 DELETE 影响范围内的数据。

​​效果​​:

其他事务无法在加锁的间隙内插入新数据,​​彻底避免幻读​​。

仅在 RR 隔离级别下生效。

不同隔离级别下的幻读行为​
RR 隔离级别下的幻读解决方案​
快照读 + MVCC(隐式解决)​​

​​场景​​:事务中仅执行普通 SELECT 查询(快照读)。

​​示例​​:

java 复制代码
-- 事务 A(RR 隔离级别)
START TRANSACTION;
SELECT * FROM users WHERE age > 20; -- 生成 ReadView,返回 10 条数据

-- 事务 B 插入新数据并提交
INSERT INTO users (age) VALUES (25);
COMMIT;

-- 事务 A 再次查询
SELECT * FROM users WHERE age > 20; -- 仍返回 10 条数据(ReadView 未更新)

结果​​:事务 A 的两次查询结果一致,MVCC 屏蔽了事务 B 的插入操作。

当前读 + 间隙锁(显式解决)​

​​场景​​:事务中执行 SELECT ... FOR UPDATE、UPDATE 或 DELETE(当前读)。

​​示例​​:

java 复制代码
-- 事务 A(RR 隔离级别)
START TRANSACTION;
SELECT * FROM users WHERE age > 20 FOR UPDATE; -- 当前读,对 age > 20 的间隙加锁

-- 事务 B 尝试插入新数据(age=25)
INSERT INTO users (age) VALUES (25); -- 被阻塞,直到事务 A 提交或回滚

-- 事务 A 提交后,事务 B 才能继续执行
COMMIT;

结果​​:事务 B 的插入操作被间隙锁阻塞,事务 A 的两次查询结果一致。

MVCC 的实现细节​

​​版本链遍历​​

从当前数据行的 DB_ROLL_PTR 开始,沿着 Undo Log 链表查找符合可见性规则的版本。

Undo Log 清理​​

当没有事务需要访问某个历史版本时,对应的 Undo Log 会被 Purge 线程清理。

​​风险​​:长事务可能导致 Undo Log 堆积,引发存储膨胀。

事务 ID 分配​​

事务 ID(DB_TRX_ID)是全局自增的,每个事务在修改数据时被分配唯一 ID。

快照读 vs 当前读​

快照读(Snapshot Read)​​

​​操作​​:普通 SELECT 语句(不加锁)。

​​实现​​:基于 MVCC 读取历史版本数据。

​​特点​​:不阻塞写操作,但可能读到旧数据。

当前读(Current Read)​​

​​操作​​:SELECT ... FOR UPDATE、UPDATE、DELETE、INSERT。

​​实现​​:读取最新数据并加锁(如行锁、间隙锁),确保操作的一致性。

​​特点​​:阻塞其他事务的并发修改,保证数据最新。

相关推荐
拐锅6 分钟前
CentOS一键安装MySQL5.7(源码安装)
mysql
阿达C2 小时前
MySQL常用函数详解及SQL代码示例
android·sql·mysql
涤生大数据2 小时前
HBase协处理器深度解析:原理、实现与最佳实践
大数据·数据库·hbase
悠悠-我心2 小时前
docker 通过定时任务恢复MySQL数据库
数据库·mysql·docker
云计算DevOps-韩老师3 小时前
Windows 10系统中找回MySQL 8的root密码
windows·mysql·adb
蔡蓝3 小时前
Mysql中索引的知识
数据库·mysql
枫叶20004 小时前
OceanBase数据库-学习笔记5-用户
数据库·笔记·学习·oceanbase
有被蠢哭到4 小时前
SQL面试之--明明建了索引为什么失效了?
数据库·sql·面试
ascarl20105 小时前
待验证---Oracle 19c 在 CentOS 7 上的快速安装部署指南
数据库·oracle·centos