目录
一 前景导入
1 当前读
可使当前事务读取的是最新版本的数据,读取时还要保证其他并发事务不能修改当中记录,会对读取的记录进行加锁。
SELECT ... LOCK IN SHARE MODE
(共享锁/S锁)SELECT ... FOR UPDATE
(排他锁/X锁)UPDATE
、INSERT
、DELETE
操作(自动加排他锁)
2 快照读
简单的select(不加锁)就是快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁。
快照读本质上就是使用MVCC机制访问数据的历史版本
隔离级别 | 快照读行为 | 当前读行为 |
---|---|---|
Read Committed | 每次 SELECT 都生成新的快照(能看到其他事务已提交的修改) | 始终读取最新已提交版本并加锁 |
Repeatable Read | 事务中第一个 SELECT 语句建立快照,后续读取都基于此快照(看不到后续修改) | 始终读取最新已提交版本并加锁 |
Serializable | 快照读退化为当前读(所有 SELECT 自动转为 SELECT ... LOCK IN SHARE MODE) | 正常当前读行为 |
二 MVCC
概念:MVCC全称:多版本并发控制。
MVCC允许多个事务同时读取同一行数据,但是确保了数据版本是当前事务开启之前的版本。(其他事务修改但是看见的版本还是修改之前的)
1 隐藏字段
在 InnoDB 的 MVCC 实现中,每条记录都包含三个关键隐藏字段,它们共同构建了多版本控制的基石:
字段名 | 大小 | 作用 | 是否必选 |
---|---|---|---|
DB_TRX_ID 事务ID | 6 字节 | 记录最后修改该行的事务 ID | 总是存在 |
DB_ROLL_PTR 回滚指针 | 7 字节 | 指向 Undo Log 中上一个版本的指针(构成版本链) | 总是存在 |
DB_ROW_ID | 6 字节 | 隐式自增主键(仅当无主键时生成) | 条件存在 |
2 UndoLog 回滚日志
(1 UndoLog日志
概念:回滚日志,用于记录数据被修改前的信息
作用:是数据库实现事务原子性和多版本并发控制的核心机制
场景化描述:
当事务需要回滚或一致性读时,内存中可能存在未提交的修改。
重启后或事务内,Undo Log 能提供旧数据版本,用于:
撤销未提交的操作(回滚)
构造历史快照(MVCC 非阻塞读)
(2 UndoLog版本链
Undo Log 版本链的核心价值正是通过精准的指针定位实现对历史版本的精确访问。
3 Read View
读视图,用于决定事务能看到哪些版本的数据。本质上是事务启动时对数据库系统状态的一次快照,解决了并发读写当中的数据可见性问题。
Read View 包含四个关键字段:
字段名 | 描述 | 作用 |
---|---|---|
m_ids |
生成 Read View 时活跃事务ID列表(未提交的事务) | 判断数据版本是否由未提交事务创建 |
min_trx_id |
活跃事务中的最小事务ID | 加速判断:事务ID < min_trx_id 一定可见 |
max_trx_id |
系统预分配的下一个事务ID(非当前最大ID) | 判断:事务ID ≥ max_trx_id 一定不可见 |
creator_trx_id |
创建该 Read View 的事务ID(当前事务自身ID) | 避免看到自己未提交的修改 |
读已提交这个隔离级别当中,在每一个select 语句执行前都会生成一个ReadView。导致出现不可重复读的现象,可能出现两次读取数据不同的情况。
可重复读是执行第一条select时,生成一个ReadView然后整个业务期间都在使用这个ReadView。读取的数据始终相同,无视其他事务的提交。
举个例子
-- 事务A (RC级别)
BEGIN; -- trx_id=100 (事务A被分配ID=100)
-- 第一次查询 (创建ReadView1)
SELECT balance FROM accounts WHERE id=1; -- 返回1000
/*
ReadView1状态:
m_ids = [100] -- 活跃事务ID列表 (当前只有事务A)
min_trx_id = 100 -- 最小活跃事务ID (m_ids中的最小值)
max_trx_id = 101 -- 下一个将分配的事务ID (当前最大事务ID+1)
creator_trx_id = 100 -- 创建此ReadView的事务ID
可见性判断过程:
假设数据行初始db_trx_id=90 (小于min_trx_id)
90 < min_trx_id(100) → 可见 → 返回1000
*/
-- 事务B (trx_id=101) 启动并提交
UPDATE accounts SET balance=900 WHERE id=1;
-- 修改后数据行:
-- db_trx_id = 101 (最后修改事务ID)
-- db_roll_ptr → 指向旧版本(trx_id=90, balance=1000)
COMMIT; -- 事务B提交,从活跃事务列表移除
-- 第二次查询 (创建新ReadView2)
SELECT balance FROM accounts WHERE id=1; -- 返回900
/*
ReadView2状态:
m_ids = [100] -- 活跃事务ID列表 (事务B已提交,只剩事务A)
min_trx_id = 100 -- 最小活跃事务ID (仍是100)
max_trx_id = 102 -- 下一个将分配的事务ID (101已使用)
creator_trx_id = 100 -- 创建者事务ID
可见性判断过程:
当前行db_trx_id=101
1. 101 != creator_trx_id(100) → 非当前事务修改
2. 101 >= min_trx_id(100) 且 101 < max_trx_id(102) → 在[mins, max)范围内
3. 检查m_ids=[100] → 101不在其中 → 已提交 → 可见
返回当前版本数据900
*/
可见性判断规则优先级:
首先检查:
db_trx_id == creator_trx_id
(当前事务自身修改)然后检查:
db_trx_id < min_trx_id
(在ReadView创建前已提交)再检查:
db_trx_id >= max_trx_id
(在ReadView创建后启动的事务)读取快照之后有事务过来修改了但是快照读取的是那一瞬间的值故才会出现大于max预分配的情况最后检查范围:
min_trx_id <= db_trx_id < max_trx_id
在m_ids中 → 未提交 → 不可见
不在m_ids中 → 已提交 → 可见
三 面试八股
介绍一下MVCC
首先我想介绍的是MVCC是什么,MVCC全称多版本并发控制,核心思想如同字面意思,为数据维护多个版本,而并非直接覆盖。
其次再说说其功能,其主要解决的是读写之间冲突而导致的并发性能问题。他让不同的事务在不同的隔离级别能看见不同的隔离级别,主要是RC与RR这两种隔离级别(RC是在每一次读取之前都会生成一个ReadView快照,而RR是只在第一次读取之前生成一个ReadView快照,RR则在一次事务当中就不会出现不可重复读的情况,而RC则会出现不可重复读的现象(因为其允许其他事务对其进行写的操作,导致读取的数据可能会出现不同,呆滞出现不可重复读))
核心:版本链+ReadView+undolog
这里说到了ReadView就涉及其原理部分了,这里就不得不提到一个隐藏字段,存储在数据行当中的db_trx_id(最后修改数据的事务id),在读取时会拿这个事务id与ReadView当中的字段进行对比,四个字段(创建当前ReadView事务的id,生成ReadView时活跃的最小事务id,生成ReadView时活跃的事务id的列表,生成ReadView时预分配的下一个事务id)
首先检查:
db_trx_id == creator_trx_id
(当前事务自身修改)可见然后检查:
db_trx_id < min_trx_id
(在ReadView创建前已提交)可见再检查:
db_trx_id >= max_trx_id
(在ReadView创建后启动的事务)不可见最后检查范围:
min_trx_id <= db_trx_id < max_trx_id
在m_ids中 → 未提交 → 不可见
不在m_ids中 → 已提交 → 可见
以上是对事务版本的判断,判断结束后如果是可见的,那么就直接返回该版本的数据作为查询结果,但是如果不可见,那么就需要用到版本链回溯到之前的版本,获取行数据的db_roll_ptr(回滚指针),指针指向上一个版本在undolog(回滚日志)当中具体位置