文章目录
- 复习 MySQL Day1:事务
- MySQL 事务的四大特性?
- 并发事务会出现什么问题?
- MySQL 事务的隔离级别?
- 不同事务隔离级别下会发生什么问题?
- MVCC 的实现原理?
- 核心数据结构
- 版本链构建示例
- 可见性判断算法
- MVCC 可见性判断总结
- 幻读如何解决?
- 读已提交隔离级别如何实现?
复习 MySQL Day1:事务
MySQL 事务的四大特性?
- 原子性:事务当中的若干条数据库操作要么全部成功,要么全部失败;
- 一致性:数据库总是从一个一致性的状态迁移到另一个一致性的状态;
- 隔离性:事务提交之前,对数据库做出的更改对其他事务不可见;
- 持久性:事务提交之后就会被持久化到磁盘当中。
并发事务会出现什么问题?
指的主要是由 MySQL 隔离级别的不同带来的若干问题。
- 脏读:事务还未提交,其所做的修改就已经可以被其他事务所读取了;
- 不可重复读:事务开始时读取的数据与事务执行过程中读取到的数据不同;
- 幻读:通常发生在区间查询的场景下,指的是在一个事务执行期间内,上一次范围查询读取到的数据与本次范围查询读取到的数据不一致,其发生的原因可能是在事务执行期间有其他事务向第一次查询的数据区间当中插入了新的数据。
MySQL 事务的隔离级别?
- 读未提交:事务还未提交,其他事务就可看到其所做的更改,隔离级别最低;
- 读已提交:事务提交之后,其所做的更改才能够被其他事务所看到。在读已提交隔离级别下,当前事务可以看到其他事务执行完成后所做的修改;
- 可重复读:MySQL InnoDB 数据引擎的默认隔离级别,指的是从事务开始到事务结束期间,读取到的数据都是一致的;
- 串行化:对记录加上读写锁,在多个事务进行读写的过程中,如果发生了锁冲突,那么当前事务必须等上一个事务读写完成方可进行读写。串行化不存在并发事务的问题,但它的并发性能最差。
不同事务隔离级别下会发生什么问题?
- 读未提交:脏读、不可重复读、幻读;
- 读已提交:不可重复读、幻读;
- 可重复读:幻读;
- 串行化:不存在并发事务的问题;
MVCC 的实现原理?
MVCC 是数据库实现并发控制的关键技术,InnoDB 数据引擎通过 MVCC 实现读操作的并发,极大提高了数据库的并发性能。
核心数据结构
隐藏字段
InnoDB 为每行记录添加了以下隐藏字段:
- DB_TRX_ID:记录创建或最后一次修改该行记录的事务 ID;
- DB_ROLL_PTR:回滚指针,指向 undo log 记录;
- DB_ROW_ID:隐含的自增行 ID;
- DELETE BIT:记录该行是否删除。
Undo Log(回滚日志)
- 存储行记录的历史版本;
- 组成版本链,通过 DB_ROLL_PTR 指针连接;
- 用于事务回滚与 MVCC 读取。
Read View(读视图)
- m_ids:生成 Read View 时活跃的事务 ID 列表;
- min_trx_id:m_ids 中的最小事务 ID;
- max_trx_id:m_ids 中的最大事务 ID;
- create_trx_id:创建该 Read View 的事务 ID。
版本链构建示例
- 每次更新操作都会在 undo log 记录旧数据版本;
- 通过 DB_ROLL_PTR 指针形成单向链表;
- 链表头是最新版本,尾部是最旧版本。
可见性判断算法
比较 DB_TRX_ID 与 creator_trx_id
如果相等,说明当前记录是该事务自身修改的事务,对当前事务可见。
检查 DB_TRX_ID < min_trx_id
说明这条记录在当前 Read View 生成之前已经提交,对当前事务可见。
检查 DB_TRX_ID >= max_trx_id
说明这条记录是在 Read View 生成之后创建的,对当前事务不可见。
检查 DB_TRX_ID 是否在 m_ids 事务活跃列表当中
- 存在:生成 Read View 时当前记录仍活跃,对当前事务不可见;
- 不存在:最后一次修改该条记录的事务已提交,对当前事务可见;
MVCC 可见性判断总结
总的来说,针对基于 MVCC 的事务可见性判断,关键的字段包括以下几个:
- 对于记录,每一条记录都有一个隐式的 DB_TRX_ID 字段,用于记录最后一个修改这条记录的事务 ID;
- 对于 SELECT 操作生成的 Read View,其隐式包含以下几个字段:
1)m_ids:Read View 生成时活跃的事务 ID 列表;
2)min_trx_id:m_ids 中最小的事务 ID;
3)max_trx_id:m_ids 中最大的事务 ID;
4)create_trx_id:创建这个 Read View 的事务 ID。
可见性判断的算法流程如下:
- 首先对比记录的 DB_TRX_ID 和 creator_trx_id,相等则代表该记录最后一次由当前视图修改,对该事务可见;
- 之后再比对 DB_TRX_ID 和 min_trx_id 以及 max_trx_id 的大小,如果小于 min_trx_id,说明修改该记录的事务在生成 Read View 时已提交,对当前事务可见;如果大于 max_trx_id,说明修改该记录的事务在当前事务之后创建,其所做的修改对当前事务不可见,通过 DB_ROLL_PTR 找到该记录的上一个版本。
- 最后查看 DB_TRX_ID 是否在 m_ids 当中,如果在,说明修改这条记录的事务在活跃列表当中,该记录的当前版本对当前事务不可见;否则说明修改该记录的事务已经提交,这条记录对当前事务可见。
幻读如何解决?
通过快照读(一致性非锁定读)
对于普通的 SELECT 查询语句,InnoDB 使用 MVCC 提供一致性视图,避免看到其他事务插入的数据。
使用间隙锁(Gap Lock)和临键锁(Next-Key Lock)
InnoDB 在可重复读隔离级别下通过以下方式防止幻读:
- 间隙锁(Gap Lock):锁定索引记录之间的间隙;
- 临键锁(Next-Key Lock):临键锁是记录锁(行锁)+ 间隙锁的组合,锁定记录及其前面的间隙。
一个基于间隙锁 + 临键锁防止幻读的例子如下:
-- 事务1
BEGIN;
SELECT * FROM users WHERE age > 20 FOR UPDATE; -- 锁定age>20的所有记录和间隙
-- ⬆️ 显式地使用区间锁锁定一个范围, 避免在范围内有新记录插入
-- 此时事务2尝试插入age>20的记录会被阻塞
INSERT INTO users(name, age) VALUES('new_user', 25); -- 阻塞
总结
使用「MVCC 版本控制」或「间隙锁 + 临键锁」这两种方式可以避免幻读的问题。
读已提交隔离级别如何实现?
在读已提交隔离级别下,每次执行 SELECT 语句都会创建一个 Read View。创建 Read View 时已经提交的事务所做的修改对当前事务是可见的(会导致不可重复读问题),但未提交以及当前事务之后的事务所做的修改不可见。