目录
一、MySQL 事务的特性
二、并行事务可能发生的问题
2.1、脏读
2.2、不可重复读
2.3、幻读
三、事务的隔离级别
3.1、隔离级别介绍
3.2、隔离级别实现
四、Read View 是如何在 MVCC 中工作的?
4.1、Read View 中四个字段的作用
4.2、聚簇索引中的两个隐藏列
五、可重复读和读提交是如何工作的?
5.1、可重复读
5.2、读提交是如何工作的?
六、MySQL可重复读隔离级别,完全解决了幻读???
6.1、什么是幻读?
6.2、幻读的两个
InnoDB 是通过什么技术来保证事物的四个特性的呢?
如果一个事务【读到】了另一个【未提交事务修改过的数据】,就意味着发生了【脏读】现象。
下面举个:
事务 A 发生了回滚,那么事务 B 刚才得到的数据就是过期的数据,这种现象就被称为脏读。
在同一个事物内多次读取同一个数据,如果出现两次读到的数据不一样的情况,就意味着发生了【不可重复读】现象。
举个:
在一个事务内多次查询某个符合查询条件的【记录数量】,如果前后两次查询到的记录数量是不一样的情况,就意味着发生了【幻读】情况。
举个:
这三种现象的严重性如:脏读 > 不可重复读 > 幻读
SQL标准提出了四种隔离机制来规避上述情况,隔离级别越高,性能越低,级别如下:
也就是说:
MySQL InnoDB 引擎的默认隔离级别虽然是【可重复读】,但是它很大程度上避免了【幻读】情况,但并不是完全解决了。
针对【快照读】(普通 select 语句),是通过 MVCC 方式解决了幻读,因为在可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事物插入了一条数据,是查询不到这条数据的,很好的避免了幻读问题。
针对【当前读】(select ....for update 等语句),是通过 next - key lock (记录锁加间隙锁)方式解决了幻读,因为在执行执行【当前读】操作时,会加上 next - key lock ,如果有其他事物在锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法插入成功,也很好的避免了幻读问题。
对于【读未提交】隔离级别的事务来说,因为可以读到未提交事务修改的数据,所以直接读取最新的数据就行。
对于【串行化】隔离级别事务来说,通过加读写锁的方式来避免并行访问。
对于【读提交】和【可重复读】隔离级别的事务来说,它们是通过 Read View 来实现的。它们的区别就是创建 Read View 的时机不同,可以把Read View 理解成一个数据快照,就像相机拍照那样定格某一时刻风景。【读提交】隔离级别是在【每个语句执行前】都会重新生成一个Read View ,而【可重复读】隔离机制是【启动事务时】生成一个Read View ,然后整个事务期间都在用这个Read View 。
对于使用 InnoDB 存储引擎的数据库表,它的聚簇索引记录中都包含两个隐藏列,举个:
trx_id :当一个事务对某条事务聚簇索引记录进行改动时,就会把该事务的事务 id 记录在 trx_id 隐藏列中。
roll_pointer :每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中,然后这个隐藏列是个指针,指向每一个旧版记录,可以通过它找到修改前的记录。
在创建 Read View 之后,我们可以将 【trx_id】划分为这三种情况:
一个事务去访问记录的时候,除了自己的更新记录总是可见之外,还存在以下情况:
通过上述【版本链】来控制并发事务访问同一个记录时的行为就叫做 MVCC(多版本并发控制)
可重复读隔离机制是启动事物时生成一个 Read View,然后整个事务期间都在用这个 Read View
假设事务A(事务 id 为 51)启动之后,紧接着事务B(事务 id 为 52)也启动了,那这两个事务创建的Read View 如下:
记录的字段:
事务 A 和事务 B 的Read View 具体内容:
在可重复读隔离级别下,事务 A 和事务 B 按顺序执行以下操作:
分析:
事务 B 在第一次读汤圆的数字记录,在找到记录后,它会先看这条记录的【trx_id】,此时发现 【trx_id】为 50,比事务 B 的Read View 中的【min_trx_id】值(51)还小,这就说明修改这条记录的事务早就在事务 B 启动之前提交过了,所以该版本的记录对事务 B 可见的,也就是事务 B 可以获取到这条记录。
接着,事务 A 通过 update 语句将这条记录修改了(还未提交事务),将汤圆的数字改为222222222,此时MySQL 会记录相应的 undo log,并以链表的形式串联起来,形成版本链如:
在上图中【记录的字段】看到,由于事务 A 修改了该记录,以前的记录就变成了旧版本记录了,于是最新记录和旧版本记录通过链表的方式串起来,而且最新记录的【trx_id】是事务 A 的事务 id(trx_id = 51)
当事务 B 第二次去读该数字时,发现这条记录的【trx_id】值为 51。在事务 B 的Read View 的【min_trx_id】和【max_trx_id】之间,则需要判断【trx_id】值是否在【m_ids】范围内,判断的结果是在,那么说明这条记录时被还未提交事务修改的,这时事务 B 并不会读取这个版本的记录。而是沿着 【undo log】链条往下找旧版本的记录,直到找到【trx_id】小于事务 B 的 Read View 中的【min_trx_id】值的第一条记录,所以事务 B 能够读取到的是 【trx_id】为 50 的记录,读到的记录也就是汤圆数字是111111111的这条记录。
最后,当事务 A 提交事务后,由于隔离级别是【可重复读】,所以事务 B 再次读取记录时,还是基于启动事务时创建的 Read View 来判断当前版本的记录是否可见。所以,及时事务 A 将汤圆的数字修改为222222222并提交了事务,事务 B 第三次读取数据时,读到的记录都是汤圆数字为111111111的这条记录。
读提交隔离级别是在每次读取数据时,都会生成一个新的 Read View。
也就是说,在事务执行期间多次读取同一数据,可能会出现前后两次读取数据不一致的情况,因为在次事务执行期间有另外的事务修改了此记录,并提交了事务。
直接举个吧~
假设事务 A(事务 id 为 51)启动后,紧接着事务 B(事务 id 为 52)也启动了,接着按照顺序执行了以下操作:
我们来分析为什么事务 B 第二次读数据时,读不到事务 A(还未提交事务)修改的数据
事务 B 在找到汤圆这条记录时,会看到这条记录的【trx_id】是 51 ,在事务 B 的 Read View 的 【min_trx_id】和【max_trx_id】之间,接下来需要判断【trx_id】值是否在【m_ids】范围内,判断结果是在,那么说明这条记录是被还未提交的事务修改的,这时事务 B 并不会读取这个版本的记录。而是沿着 【undo log】链条往下找旧版本的记录,直到找到【trx_id】小于事务 B 的 Read View 中的【min_trx_id】值的第一条记录,所以事务 B 能读取到的是【trx_id】为 50 的记录,也就是汤圆的数字为111111111
那为什么事务 A 提交之后,事务 B 就可以读到事务 A 修改的数据呢?
在事务 A 提交之后,由于隔离级别是【读提交】,所以事务 B 在每次读数据的时候,会重新创建 Read View ,此时事务 B 第三次读取数据创建的 Read View长这样:
事务 B 在找到汤圆这条记录时,会发现这条记录的 trx_id 是51,比事务 B 的 Read View 中的【min_trx_id】的值(52)还小,这就说明这条记录在创建 Read View 前提交过了,所以该版本的记录对事务 B 是可见的。
也正是因为在读提交隔离级别下,事务每次读数据时都重新创建 Read View,那么事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务。
在上述中我们提到了MySQL InnoDB 引擎的默认隔离级别虽然是【可重复读】,但是很大程度上避免了幻读但未完全解决:
当同一个查询在不同的事件产生不同的结果集时,事务中就会出现所谓的幻象问题。:如果 select 执行了两次,但第二次返回了第二次没有返回的行,则该行是“幻像行”。
此时表中有四条记录 id 号分别为 1,2,3,4 中间字段为上图字段一样。
场景①:
在可重复读隔离级别下,事务 A 第一次执行普通的 select 语句时生成了一个 Read View ,之后事务 B 向表中插入了一条 id = 5 的记录并提交。接着事务 A 对 id = 5 这条记录进行了更新操作,这个时刻,这条新纪录的 【trx_id】隐藏列的值就变成了事务 A 的事务 id,之后事务 A 再用普通 select 语句去查询这条记录时就可以看到这条记录了,于是发生了幻读。
场景②:
T1时刻:事务 A 先执行【快照读语句】:select * from t_test where id > 100 得到了 3 条记录
T2时刻:事务 B 插入一个 id = 200 的记录并提交
T3时刻:事务 A 再执行【当前读语句】:select * from t_test where id > 100 for update 就会 得到4条记录