FXJ Wiki

Back

MySQL(II):事务与MVCCBlur image

MySQL 事务、锁、MVCC 与日志#

事务#

事务的特性#

ACID 特性以及通过怎样的方式来保证。

  • 原子性:通过undo log(回滚日志)保证
  • 一致性:通过其他三个特性来保证
  • 隔离性:通过锁机制/MVCC(多版本并发控制)保证
  • 持久性:通过 redo log(重做日志)保证

事务的隔离级别#

  1. 读未提交(read uncommitted),指一个事务还没提交时,它做的变更就能被其他事务看到;
  2. 读提交(read committed),指一个事务提交之后,它做的变更才能被其他事务看到;
  3. 可重复读(repeatable read),指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,MySQL InnoDB 引擎的默认隔离级别;
  4. 串行化(serializable );会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;

一般来讲,使用可重复读(默认)就可以很大程度上避免幻读的问题了(但是还是可能出现),串行化隔离级别,对性能会有影响,主要是通过下面两个方式基本解决幻读问题:

  1. 对于普通的 select 语句(快照读),通过 MVCC 解决了幻读
  2. 对于 select…for update 语句(当前读),通过 next-key lock(记录锁+间隙锁)解决幻读

实现方式:

  • 对于「读未提交」隔离级别的事务来说,因为可以读到未提交事务修改的数据,所以直接读取最新的数据就好了;
  • 对于「串行化」隔离级别的事务来说,通过加读写锁的方式来避免并行访问;
  • 对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过 Read View 来实现的,它们的区别在于创建 Read View 的时机不同,大家可以把 Read View 理解成一个数据快照,就像相机拍照那样,定格某一时刻的风景。「读提交」隔离级别是在「每个语句执行前」都会重新生成一个 Read View,而「可重复读」隔离级别是「启动事务时」生成一个 Read View,然后整个事务期间都在用这个 Read View。

MySQL 中开启事务的命令:

  1. begin/start transaction:当执行了第一句 select 语句才算真正开启
  2. start transaction with consistent snapshot:立即开启

脏读/不可重复读/幻读问题#

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

不可重复读:同一个事务中多次读取同一个数据,出现前后两次读到的数据不一样的情况

幻读:同一个事务中多次查询某个符合查询条件的记录数量,出现前后两次查询到的记录数量不一样的情况

MVCC(重)#

Read View 在 MVCC 里如何工作的? Read View:

在创建 Read View 后,将记录中的 trx_id 划分为这三种情况:

这种通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)。

简单来说,MVCC 这条链路可以直接这样记:

  • 行记录里会带上 trx_idroll_pointer
  • trx_id 表示最后一次修改这行的事务 id;
  • roll_pointer 会把当前版本指到上一个 undo log 版本;
  • 因而一条记录会顺着 undo log 串成版本链;
  • 快照读时,事务拿着自己的 Read View 沿着版本链往前找,直到找到自己能看到的那一版。

从苹果备忘录里补几个最容易被追问的点:

  • undo log 不只是给回滚用的,也是 MVCC 历史版本真正的来源。
  • 对于 delete,InnoDB 不是立刻物理删除,而是先打删除标记,后续再由 purge 线程清理。
  • 对于 update
    • 如果更新的是主键列,本质上更接近“删旧行 + 插新行”;
    • 如果更新的是普通列,则在 undo log 里记录旧值,回滚或快照读都能沿版本链拿到历史版本。
  • undo page 自己也会进 Buffer Pool,真正的持久化仍然要靠 redo log 兜底。

当前行 -> trx_id / roll_pointer -> undo log 旧版本 -> Read View 判断可见性 -> 找到事务可见版本

Read View 的可见性判断#
  • trx_id < min_trx_id:生成快照时,这个事务已经提交了,当前版本可见。
  • trx_id >= max_trx_id:这是快照之后才分配的事务,当前版本不可见,要往前找旧版本。
  • min_trx_id <= trx_id < max_trx_id
    • 如果 trx_id 在活跃事务列表里,说明它创建快照时还没提交,不可见;
    • 否则说明已经提交,可见。
MVCC 是如何实现已提交/可重复读的?#

对于读已提交:

  • 每次执行 SELECT 语句时都会重新生成一个 Read View。

对于可重复读:

  • 在事务启动时(第一次 SELECT 或 BEGIN 时)生成一个 Read View,并且该 Read View 在整个事务的生命周期内都有效,不再重新生成。
哪种情况下 MVCC 不能完全避免幻读?#
## 事务 A-----------------
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from t_stu where id = 5;
Empty set (0.01 sec)

## 事务 B-----------------
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into t_stu values(5, '小美', 18);
Query OK, 1 row affected (0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.00 sec)

## 事务 A-----------------
mysql> update t_stu set name = '小林coding' where id = 5;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from t_stu where id = 5;
+----+--------------+------+
| id | name         | age  |
+----+--------------+------+
|  5 | 小林coding   |   18 |
+----+--------------+------+
1 row in set (0.00 sec)
sql

Attention:主要还是因为 MVCC 只支持 select,所以对于有 update 的情况也束手无策…

不过可以通过 MVCC+next-key-lock 来彻底解决幻读!

InnoDB 存储引擎在 RR 级别下通过 MVCC 和 Next-key Lock 来解决幻读问题:

1、执行普通 select,此时会以 MVCC 快照读的方式读取数据

在快照读的情况下,RR 隔离级别只会在事务开启后的第一次查询生成 Read View ,并使用至事务提交。所以在生成 Read View 之后其它事务所做的更新、插入记录版本对当前事务并不可见,实现了可重复读和防止快照读下的 “幻读”

2、执行 select…for update/lock in share mode、insert、update、delete 等当前读

在当前读下,读取的都是最新的数据,如果其它事务有插入新的记录,并且刚好在当前事务查询范围内,就会产生幻读!InnoDB 使用 Next-key Lock 来防止这种情况。当执行当前读时,会锁定读取到的记录的同时,锁定它们的间隙,防止其它事务在查询范围内插入数据。只要我不让你插入,就不会发生幻读

MySQL(II):事务与MVCC
https://fxj.wiki/blog/interview-mysql-2
Author 玛卡巴卡
Published at 2025年6月5日
Comment seems to stuck. Try to refresh?✨