这篇我以为有点难度,我会更慢的更详细的分析一些 case 。mysql
MySQL 的默认事务隔离级别和其余几个主流数据库隔离级别不一样,他的事务隔离级别是 RR(REPEATABLE-READ) 其余的主流数据库好比 oracle 一般是 RC(READ-COMMITTED)sql
关于数据库有哪些隔离级别我这里就不详细阐述了,大概是什么特性我这里就不阐述了你们能够自行翻阅资料,让咱们聚焦这两个最重要的隔离级别在一些查询更新的时候会出现什么样的特性表达。数据库
当咱们使用 RR 的时候,事务启动的时候会建立一个视图 read-view,以后事务执行期间,即便有其余事务修改了数据,事务看到的仍然和她启动的时候看到的同样。也就是说,一个在可重复读隔离级别下执行的事务不受外界影响。数组
可是上一篇分享锁的文章里面咱们也提到了,若是说另一个事务对表加了行锁,他会被锁住进入等待状态。那么当等待状态结束,这个事务本身要获取行锁更新数据的时候,他读到的值是什么呢?oracle
来看个例子性能
mysql> CREATE TABLE `t` ( `id` int(11) NOT NULL, `k` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB; insert into t(id, k) values(1,1),(2,2);
而后使用这个事务启动顺序来测试测试
这里有几个点须要注意,咱们在数据库使用事务 begin/start transaction 命令并非一个事务的起点,在执行到第一个操做 InnooDB 表的语句,事务才被真正启动。spa
若是咱们要立刻启动一个一致性读事务使用 start transaction with consistent snapshot 这个。code
它的含义是:执行 start transaction 同时创建本事务一致性读的 snapshot . 而不是等到执行第一条语句时,才开始事务,而且创建一致性读的 snapshot 。blog
文章中说,这个顺序查询,事务 B 查询到的 id =1 的 k 值是3,事务 A 查询到的值 k 值是 1。我起初也没法理解,下面让咱们一步一步来得出结论。
快照”在 MVCC 里是怎么工做的?
在可重复读隔离级别下,事务在启动的时候就拍了快照。InnoDB 里面每一个事务有一个惟一的事务 ID, 叫 transaction id。它是在事务开始的时候向 InnoDB 的事务系统申请的。是按照申请顺序递增的。每行数据也有多个版本,每次事务更新数据的时候,都会生成一个新的数据版本,而且把 transaction id 赋值给这个数据版本的事务 ID, row trx_id。旧的数据版本要保留,而且新的数据版本中,可以有信息能够直接拿到。
当前最新版本是 V4 V4 版本是通过一系列更新以后获得的最新的状态。 他的 row trx_id = 25。
u1 u2 u3 都是 undo log 的记录,咱们能够在 v4 经过 undolog 恢复到版本v1 v2 v3 并非物理上真实存在的。
这里按照可重复度的定义,当一个事务启动的时候,能偶看到全部已经提交的事务结果。可是以后,这个事务执行期间,其余事务的更新对它不可见。
所以一个在 RR 事务级别启动一个事务的时候声明说,以我启动的时刻为准,若是一个数据版本是在我启动以前生成的就认,若是是我启动以后才生成的,就不认,我必须找到他的上一个版本。若是上一个版本也不可见,就继续往前找。固然若是是这个事务本身自己更新的数据,它本身是要认的。
在实现上, InnoDB 为每一个事务构造了一个数组,用来保存这个事务启动的时候,当前正在“活跃”的全部事务 ID,“活跃”指的是,启动了可是尚未提交。
这个数组组成了一个相似这样的东西
低水位:指获取到这个数组内的 trx_id 最小值。
高水位:指获取到的这个数组内的 trx_id 最大值 + 1
这样对于当前事务启动的瞬间来讲,一个数据版本的 row trx_id 有如下几种可能。
1. 若是落在绿色部分,标示这个版本是已经提价的事务护着当前本身事务生成的,这个数据是可见的。
2. 若是落在红色部分,标示这个版本是由未来的事务生成的,是不可见的。
3. 若是落在黄色部分,
a. 若是 row trx_id 在数组中,标示这个版本是由尚未提交哦的事务生成的,不可见。
b. 若是 row trx_id 不在数组中,标示这个版本是已经提交了的事务生成的,可见。
下面咱们用上面的理论拉来解释一下为何我第一张图的查询结果会是那个样子。
1. 假设 事务 A 开始前,系统里面没有哦活跃事务 ID .
2. 事务 A 开始时候的事务版本号为 100 事务 B 开始时候的事务版本号为 101 事务 C 开始时候的事务版本号为 102。
3. 三个事务开始前 id =1 k =1 的数据 row trx_id 是 90.
事务 A 开始的时候事务数组为 [100]
事务 B 开始时候的事务数组为 [100, 101]
事务 C 开始时候的事务数组为 [100, 101, 102]
祖先版本是 id =1, k=2 对应版本是 90。
第一个有效更新的事务是 C 当 C 完成更新以后 102 版本就是对应 id = 1 k = 2.
这个时候因为版本 102 不管对于事务 B 仍是 事务 A 都处于高水位,因此都是不见的。也就是说如今咱们执行
select * from t where id = 1 会发现
mysql> select * from t; +----+------+ | id | k | +----+------+ | 1 | 1 | | 2 | 2 | +----+------+
这个时候第二个有效事务 B 更新了,把数据从 id =1 k = 2 变动为 id =1 k=3,这个时候数据的最新版本变成了 101,而102 变成了历史版本。
注意这个时候事务 B 会发现本身的数据没有通过 k=2 这一步 直接就变成 k =3 了。。。由于事务 C 更新而且提交了,咱们在这个基础上增长会读取到 102 的更新。
可是事务 A 仍是没法读取到 102 版本和 101 版本的更新,由于他们都在高水位,因此最终读取到的仍是 id =1 k=1。
可是真实的状况 事务 A 是会去判断的,也就是说他会找到最后一个被更新的版本 101 会发现是高水位不可见。
接着找上一个版本 102 仍是高水位不可见。
最后找到原始版本 90 处于低于低水位的区域可见。
这样执行下来,虽然期间这一行数据被修改过,可是事务 A 不管在何时查询,看到这行数据的结果都是一致的,因此咱们称为一致性读。
RR 的更新数据都是先读后写的,这个读就是当前读。这能够解释为何咱们能够跳过 k=2 直接 k=3。由于在 k=1 的时候进行当前读发现 k=2 了,而后再 +1 就 k=3 了。
固然 select xx for update | lock in share mode 也是当前读。
我以为 此片文章到此就差很少了。感受老师后面以紧接着介绍了一些可有可无的东西 包括 RC 的状况。给原本就比较难以理解的状况搞得更复杂了。
如今我解除到大部分公司的 DB 使用 MySQL 都会将事务隔离级别从默认的 RR 设置到 RC,更好理解也能够更方便的用乐观锁来保证数据的一致性。而且我感受若是不使用当前读,可能还会对性能有必定的影响。毕竟上面介绍到的流程里面,是须要扫 undolog 参与的,感受这些可能都会有必定的性能损失。
Reference:
本读书笔记皆来自发布在极客时间的 林晓斌(丁奇)的 MySQL 实战45讲:
极客时间版权全部: https://time.geekbang.org/ 版权全部:
https://time.geekbang.org/column/article/70562