读未提交(Read Uncommitted):某个事务读到了其他还未提交的事务对数据所作的修改,也就是某个事务只要修改了数据,其他事务就可以看到所作的修改。
这种隔离级别下会发生脏读、不可重复读、幻读。
读提交(Read Committed):某个事务提交之后,才可以被其他事务看到。
这种隔离级别下会发生不可重复读、幻读。
可重复读(Repeatable Read):一个事务在执行过程中看到的数据,总是跟这个事务在启动的时候看到的数据是一致的。
MySQL的默认隔离级别,这种隔离级别下会发生幻读。
不可重复读侧重于数据的修改,幻读侧重于数据的新增。假设事务A开始之前某个字段的值是A,事务B将它的值修改为了B,此时事务A再次查询得到的结果是B,与事务A开始时查到的值A不一致,就是不可重复读。假设事务A开始之前查询某个表只有一条记录,事务B往表中又插入了一条记录,此时事务A再次查询莫名多出一条记录,就是幻读。
串行化(Serializable):对于同一条记录,读会加读锁,写会加写锁,读和读之间不会冲突,但是读和写之间会冲突,假如读写冲突,后访问的事务需要等待前一个事务执行完毕才可以继续执行。
因为不允许事务并发执行,只能串行执行,因此不会有脏读、不可重复读、幻读问题,但是由于串行执行,性能比较差。
InnoDB中,每个事务在开始的时候都会申请一个事物id,这样每个事务会有一个唯一的事务id,每行数据也会有多个版本,每次事务更新的时候,会生成对应的事务版本,将事物id设置到这个事务版本的trx_id中,旧的事务版本也会保留:
注意:图中的版本链不是物理上真实存在的,实际上是根据undo log版本链计算出来的。
undo log版本链
由于对数据的修改会生成undo log回滚日志,每个事务又会生成对应的事务版本,那么多个事务修改的时候就会形成一个undo log 版本链,每条数据包含两个隐藏字段,trx_id和roll_pointer:
trx_id:最近一次更新这条数据的事务id
roll_pointer:指向当前事务之前生成的那个undo log
所以多个事务串行执行的时候,每个事务都会生成一条undo log,通过roll_pointer将undo log串起来,形成undo log版本链,当没有事务需要用到这些undo log时,undo log才会被删除,假如有长事务,由于随时可能访问数据库的任何数据,在这个事务提交之前,它可能用到的undo log都将会保留,就会导致大量的占用存储空间,因此需要尽量避免长事务。
一致性视图
事务在启动的时候,InnoDb会为每一个事务创建一个数组,保存这个事务启动时,当前所有正在活跃(事务已启动但是还没提交)的事务ID,数组中事务ID最小值为低水位,已经生成过的事务的最大值的ID加1记作高水位,视图数组和高水位组成了当前事务的一致性视图,基于事务的trx_id和一致性视图可以实现读提交和不可重复读这两个隔离级别:
注:图片来自于极客时间 --- 林晓斌(丁奇):MySQL实战
对于当前事务来说,一个事务的id可能有以下三种情况:
1.在视图数组中已提交的事务中,表示这个版本是已提交的事务,那么它是可以被当前事务读到的;
2.在未开始事务中,表示这个版本的数据是由将来启动的事务生成的,对当前事务来说必然是不可见的;
3.在未提交的事务集合中,有两种情况:
(1)如果trx_id在当前事务的活跃数组中,表示这个版本是由未提交的事务生成的,对当前事务来说不可见;
(2)如果trx_id不在当前事务的活跃数组中,表示这个版本的事务已提交,对当前事务来说可见;
基于ReadView实现读提交
假设系统当前存在两个活跃的事务B和事务C:
(1)事务B第一次查询id值时会顺着undo log版本链寻找,首先会读取undo log版本链最新的值,undo log版本链中当前值为30,对应的trx_id 为30,trx_id比事务B中max_trx_id小,说明事务B在生成read view之前就存在这个活跃的事务,由于trx_id为30的事务在事务B的活跃列表中
在读提交隔离级别下,是不能读取到30这个值的,继续顺着版本链往前找,下一个是trx_id为10更新的值,trx_id =10 比事务B中的min_trx_id小,说明事务B生成read view之前,trx_id为10的事务就已经提交,所以可以读取trx_id=10的事务更新的值,事务B第一次查询得到ID的值为10;
(2)事务B在第二次查询时,会重新生成read view,此时由于事务C已提交,trx_id为30的事务不在事务B的活跃事务列表中,此时事务B是可以读取到30的,所以事务B第二次读取到ID的值为30;
实现读提交比较重要的一点就是每次在查询时都会重新生成read view。
基于ReadView实现可重复读
假设系统当前存在两个活跃的事务B和事务C:
(1)与读提交中的第一次查询一致,得到的值为10;
(2)事务B在第二次查询时,会继续使用第一次查询生成的一致性视图,此时虽然事务C已提交,但是由于没有重新生成一致性视图,trx_id为30的事务依旧在事务B的活跃事务列表中,此时事务B是不可以读取到30的,所以事务B第二次读取到ID的值为10;
实现不可重复读比较重要的一点就是整个事务都会使用一个一致性视图,不是每次查询都重新生成一致性视图。
总结:
在读提交隔离级别下,每个语句执行前都会重新算出一个一致性视图。
在可重复读隔离级别下,只在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图。
当前读
当前读指的是读取undo log版本链中最新的记录
select xx from xx lock in share;
select xx from xx for update;
lock in share mode会加读锁,for update会加写锁,这两种语句都会进行当前读。
快照读
指的是按照生成的一致性视图读取数据。
需要注意的时,如果是更新操作都是当前读。
创建持续整个事务的一个一致性快照:
start transaction with consistent snapshot;
在读提交隔离级别下,没有意义,等效于普通的start transaction。
参考:
极客时间 --- 林晓斌(丁奇):MySQL实战
救火队队长:从零开始带你成为MySQL实战优化高手