锁相关的知识我们已经学习完了,在其中我们提到过一个概念,那就是 MVCC 。这又是个什么东西呢?今天我们就来好好看看 MVCC 到底是干嘛的。
MVCC 多版本并发控制,它主要是控制 读 操作,是一种 乐观锁 场景,解决 读-写 问题。在数据库中,事务主要处理的就是 读-读、读-写、写-读 所导致的不一致问题。而 MVCC 处理的正是其中的 读-写 问题。写-读 问题就是 X 锁解决的,这个相信大家在之前的学习中已经了解到了。MVCC 不加锁,所以它是一种 乐观锁 的实现。它不阻塞并发读,与 临界锁 一起在 RR 级别解决幻读问题。
我们先来看一下读数据的几种情况。
很明显,MVCC 走的就是快照读的方式。我们为数据行的多个版本实现数据的并发读,就需要一种多版本管理机制。MVCC 可以查询到一些正在被另外一个事务执行更新的行,并且可以看到它们被更新之前的值,这样在查询的时候就不用去等待另一个事务释放锁了。
注意,在 READ UNCOMMITTED 和 SERIALIZABLE 这两个级别中,一个是不走事务,一个是串行化的执行事务,它们都是执行的 当前读 。因为最低级别我们不关心一致性问题,而最高级别则是序列执行,不会有一个事务操作另一个事务更新的情况。因此,MVCC 的场景就是在 READ COMMITTED 和 REPEATABLE READ 这两个事务隔离级别中。
MVCC 的实现主要依赖于它所包含的三块内容,它们分别是:
接下来,我们就分别来看看这三部分内容。
当我们创建表时,会为表指定字段,这个想必不用我多说了,大家都会,也都明白是什么意思。但是,这个新建的表中,其实还存在着几个隐藏的字段。
当我们改变行记录时,DB_TRX_ID 会记录下来当前的 事务id ,然后把旧的版本的数据写入到 Undo Log 中,DB_ROLL_PTR 指向上一次的旧记录。没错,DB_ROLL_PTR 就是形成了一个链表,这个链表就是 版本链 。
哪里能看到这几个隐藏字段呢?首先找到你的数据库数据存放目录,然后找到你要查看的表,使用 ibd2sdi 工具就可以查看到。
root> cd /usr/local/var/mysql8/blog_test
root> ibd2sdi tt.ibd
// ……
"columns":[
// ……
// ……
]
// ……
在结果集中的 columns 字段下,就是我们创建的表的字段信息,最后三个字段就是 DB_TRX_ID、DB_ROLL_PTR 和 DB_ROW_ID(如果有设置主键,就不会有 DB_ROW_ID)。
现在我们已经知道了,通过 DB_ROLL_PTR 隐藏字段,可以在 Undo Log 中形成一个链式数据结构,也就是我们最终的 版本链 这个东西。比如我们有四个事务对数据进行修改,就像下面这张图一样。
左侧是我们的事务操作流程,右侧上方的黄框内是行数据,或者如果是当前读,那么现在这条数据的内容就是 事务4 的数据。右侧下方是记录在 Undo Log 中的日志数据。
当 事务1 执行并修改数据时,DB_TRX_ID 指定为 事务1 的 ID ,DB_ROLL_PTR 是空的,因为在 事务1 之前没有别的事务在运行。接着,事务2 执行并修改数据,DB_TRX_ID 指定为 事务2 的 ID ,DB_ROLL_PTR 指向 事务1 。为什么 事务2 要指向 事务1 ?因为我们是同时 BEGIN 这四个事务的,在 事务2 修改数据的时候有 事务1 的修改记录。
依次类推,最终数据稳定在 事务3 提交的数据上,注意我们的 事务4 只是查询,没有修改更新数据。
这个,就是 MVCC 定义中,多版本 这个词的概念。有了 多版本 的这个 版本链 之后呢?那就是管理控制了。我们马上要讲的 ReadView 读视图配合事务隔离级别,就形成了 版本并发控制 。
上面多个事务对同一个行记录进行更新会产生多个历史快照,它们保存在 Undo Log 中,而 ReadView 就是事务在使用 MVCC 机制进行快照读时产生的 读视图 ,也就是确定读出来的是 Undo Log 中的哪条数据。另外,当事务启动时,也会生成数据库系统当前的一个快照,InnoDB 为每个事务也都构造了一个数组,用来记录并维护系统当前的活跃事务(启动了还没提交的事务)的 ID 。
总之,两种快照,一个是系统当前正在运行的事务快照,一个是事务中针对读操作从 Undo Log 中选取的一个快照。数据库系统会通过当前事务的状态,未完成事务的情况,以及 版本链 中的记录,最后根据 事务隔离级别 来选择合适的数据生成 ReadView 。
上面的描述看着很晕吧?没事,到最后我们看到结果的时候就会恍然大悟了。不过首先,我们要来看一下 ReadView 中包含哪些内容。
根据 ReadView 的内容,再根据下面的规则进行比对,从而获得当前事务是否能访问版本链上的某条记录。
内容和规则能看明白吗?如果看不明白可以多看两遍,只要达到了规则条件,那么我们所获得的,或者说是 SELECT 出来的数据内容,就是最新的这条符合规则的数据。
我们上面学习到的所有内容,最后总结一下 ReadView 的执行步骤。
好了,理论上 MVCC 的相关内容我们都说完了。最后一点则是事务隔离机制的不同,对于 MVCC 的影响也有不同。之前我们说过 REPEATABLE READ 是能防止 幻读 情况发生的。为什么能防止呢?原因就在于它的 快照读 机制与 READ COMMITTED 不同。
简单点概括。
READ COMMITTED 每次都获取生成 ReadView 。如果有别的事务提交了,那么在当前事务会读到新的数据。
REPEATABLE READ 只有第一次 SELECT 时生成 ReadView ,后面的 SELECT 语句只会读取第一条的 ReadView ,不会产生新的 ReadView 。也就是说,如果有别的事务提交了,那么当前事务读到的内容也不会发生变化。
我们可以通过代码来看它们的区别。
-- 事务1、事务2
mysql> begin;
-- 事务1、事务2
mysql> select * from tran_innodb where id = 5;
+----+------+------+
| id | name | age |
+----+------+------+
| 5 | Joe | 12 |
+----+------+------+
1 row in set (0.00 sec)
-- 事务1
mysql> update tran_innodb set name = 'Joe5';
Query OK, 19 rows affected (0.00 sec)
Rows matched: 19 Changed: 19 Warnings: 0
mysql> select * from tran_innodb where id = 5;
+----+------+------+
| id | name | age |
+----+------+------+
| 5 | Joe5 | 12 |
+----+------+------+
1 row in set (0.00 sec)
-- 事务2
mysql> select * from tran_innodb where id = 5;
+----+------+------+
| id | name | age |
+----+------+------+
| 5 | Joe | 12 |
+----+------+------+
1 row in set (0.00 sec)
-- 事务1提交
mysql> commit;
-- 事务2
mysql> select * from tran_innodb where id = 5;
+----+------+------+
| id | name | age |
+----+------+------+
| 5 | Joe5 | 12 |
+----+------+------+
1 row in set (0.00 sec)
在 READ COMMITTED 级别下,事务1、2同时开启事务,第一次也都查询完,数据是一样的。接着事务2修改了数据,将 name 修改为 Joe5 ,然后事务2自己查询了一下,数据是最新的(规则1)。事务1这时也查了一下,还是老样子数据没变(事务1未提交,Undo Log 已记录,但 trx_ids 列表中还存在,规则 4)。继续向下,事务2提交,DB_TRX_ID 变动,事务1再次查询,与新的 ReadView 进行规则比对,DB_TRX_ID 小于当前的 事务id (规则4.2),返回最新数据,查询到的数据产生了变化。
READ COMMITTED 每次 SELECT 都获取生成 ReadView。
-- 事务1、事务2
mysql> begin;
-- 事务1、事务2
mysql> select * from tran_innodb where id = 5;
+----+------+------+
| id | name | age |
+----+------+------+
| 5 | Joe | 12 |
+----+------+------+
1 row in set (0.00 sec)
-- 事务1
mysql> update tran_innodb set name = 'Joe5';
Query OK, 19 rows affected (0.00 sec)
Rows matched: 19 Changed: 19 Warnings: 0
mysql> select * from tran_innodb where id = 5;
+----+------+------+
| id | name | age |
+----+------+------+
| 5 | Joe5 | 12 |
+----+------+------+
1 row in set (0.00 sec)
-- 事务2
mysql> select * from tran_innodb where id = 5;
+----+------+------+
| id | name | age |
+----+------+------+
| 5 | Joe | 12 |
+----+------+------+
1 row in set (0.00 sec)
-- 事务1提交
mysql> commit;
-- 事务2
mysql> select * from tran_innodb where id = 5;
+----+------+------+
| id | name | age |
+----+------+------+
| 5 | Joe | 12 |
+----+------+------+
1 row in set (0.00 sec)
同样的操作,但是,最后查询到的结果没有发生变化,还是和第一次查询的结果是一样。也就是说:
REPEATABLE READ 只有在事务内第一次 SELECT 获取生成 ReadView,之后的 SELECT 不会再生成。
今天的内容难度有点大吧,而且我觉得自己也有一些概念是比较模糊的,可能会有遗漏或者错误的地方,也请大家海涵。不过总体来说大方向应该是没有问题的,如果有纰漏的地方,也希望大家在评论区随时指出。同时也希望各位大佬可以自己去查找更多的参考资料来加深对这一块的理解。
好了,事务、事务隔离机制、锁、MVCC 都讲完了,整个事务这一块最核心的内容也就学习得差不多啦。接下来我们还要学习一个内容,也是今天的文章中反复提到的内容:Undo Log,同时也会将 Redo Log 和它一起讲解学习。整完这俩货,事务这一大块的内容才算是彻底完成了,大家加油哦,胜利就在眼前。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有