前面一篇博客已经了解了事物的一些特征,这里先来看如下的一个执行流程:
先创建表:
mysql> create table k (
-> id int primary key,
-> k int(11) not null)
-> engine = innodb;
Query OK, 0 rows affected (0.36 sec)
mysql> insert into k values((1,1),(2,2));
执行上表的操作,这里先注意一下:
start transaction with consistent snapshot代表马上启动一个事物,begin/start transaction 命令并不是一个事物的起点,在执行到他们之后的第一个操作InnoDB表的语句,事物才真正启动;
两者的区别:
在C中没有显示的使用start来启动事物,但update t set k=k+1 where id = 1就是一个事物,执行完这条语句就会自动提交;(数据库中默认的autocommit为on,所以每个语句都是一个事物)
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set, 1 warning (0.10 sec)
在MySQL中,有两个视图的概念:
对于上面的执行(隔离级别为RR),事物A的查询结果是k=1;
事物B的查询为k=3;
这里对于事物A的查询结果一般不会有疑问,但对于B,为什么k会为3不是2呢?事物C的更新不是在事物B启动之后吗,不应该对B不可见吗?
这里就要注意一个东西,如果此时B的更新不在最新的值上进行,事物C的更新就会丢失,因此,事物B此时set k = k+1是在(1,2)的基础上进行的操作;
总结以上得到如下规律:
对于上面的案例,假如当前事物的隔离级别是读提交,那么结果又会是什么?
事物A查询语句返回的是k=2;因为在事物A查询的时候,事物B还未提交,所以不可见它的那行更新,而事物C的更新在A查询前就提交了,自然是可见的;
事物B的查询结果为3;
而读提交的逻辑和可重复读的逻辑类似,它们最主要的区别是:
在可重复读的隔离级别下,事物在启动的时候就拍了个快照,这个快照是基于整个数据库的;
可能你会觉得拍了一个整个数据库的快照有点不现实,如果一个数据库很大(100G),岂不是启动一个事物就得拷贝所有的数据(100G)出来,这得有多慢啊,但事实上事物执行起来却很快呀,这到底是为什么呢?
事实上,我们并不需要拷贝出整个数据库的数据,先来看看这个快照是如何实现的;
InnoDB里面每个事物有一个唯一的事物ID,叫做transaction id,它是在事物开始的时候向数据库申请的,是按申请顺序严格递增的;
在数据库中,表中的每行数据也都是有多个版本的,每次事物更新的时候,就会生成一个新的数据版本,并把当前事物的transaction id赋值给这个数据版本的事物ID,记为row trx_id;同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它;
下面这张图,就是一个记录被多个事物连续更新后的状态:
上图中最新的版本是V4,图中的虚箭头就是一个undo log(回滚日志) ,图中V1、V2、V3的值都不是真实存在的,都需要根据最新的值和回滚日志来进行计算;
回滚日志(undo log):在MySQL中,实际上每条记录在更新的时候都会同时记录一条回滚操作,记录上的最新值都可以通过回滚操作来计算出前一个状态的值,undo log记录的是逻辑日志,可以认为delete一条记录的时候,回滚日志会记录对应的insert操作,当update一条记录时,回滚日志会记录对应相反的update操作,当没有事物需要用到这些回滚日志的时候,回滚日志就会删除,对应系统里没有比这个回滚日志更早的read-view的时候就会删除对应的undo log;
上面介绍了多版本和row trx_id的概念,这时我们再来分析InnoDB是如何定义那“100G”的快照的:
有了上面的概念,对于可重复读这个隔离级别,一个事物在启动的时候只要声明:“以我启动的时刻为准,如果一个数据版本是在我启动之前生成的,就可见,如果是我启动以后才生成的,就不可见,我必须找到它的上一个版本,知道可见的那个版本为止”;
在实现上, InnoDB 为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事物 ID。“活跃”指的就是,启动了但还没提交。 数组里面事务 ID 的最小值记为低水位,当前系统里面已经创建过的事务 ID 的最大值加 1 记为高水位。 这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)。 而数据版本的可见性规则,就是基于数据的 row trx_id 和这个一致性视图的对比结果得到的。
一个数据的版本row trx_id可以有三种情况:
InnoDB 的行数据有多个版本,每个数据版本有自己的 row trx_id,每个事务或者语句有自己的一致性视图。普通查询语句是一致性读,一致性读会根据 row trx_id 和一致性视图确定数据版本的可见性。 对于可重复读,查只承认在事务启动前就已经提交完成的数据; 对于读提交,查询只承认在语句启动前就已经提交完成的数据; 而当前读,总是读取已经提交完成的最新版本。 你也可以想一下,为什么表结构不支持“可重复读”?这是因为表结构没有对应的行数据,也没有 row trx_id,因此只能遵循当前读的逻辑。 当然,MySQL 8.0 已经可以把表结构放在 InnoDB 字典里了,也许以后会支持表结构的可重复读。