隔离性与隔离级别
事物的隔离级别有四种,分别是:读未提交、读提交、可重复读、串行化,从前往后隔离级别越来越高,但是执行效率越来越低(涉及锁等情况)。
- 读未提交,即事务未提交,其他事务可以查看我这个事务修改后的值。读未提交存在脏读、不可重复读、幻读问题。
- 读提交,即事务未提交,其他事务不可以查看我当前事务修改后的值,读提交解决了读未提交中脏读问题。
- 可重复读,即事务执行过程中,就算别的事务修改了某项数据,本事务读取该数据应该是不变的,可重复读解决了读提交中不可重复读的问题。
- 串行化,即在事务执行全程加锁解决,串行化则解决了幻读的问题。
在MySQL中InnoDB默认隔离级别是可重复读。
MVCC
InnoDB的串行化是通过加锁实现的,而读提交和可重复读是通过MVCC(多版本并发控制)实现的。
当事务开始(可重复读)或者语句执行(读提交)时,会创建一个Read View(视图),视图的内容如下:
- createor_trx_id:当前事务ID,也是创建该Read Virw的事务id(按申请顺序严格递增)。
- m_ids:事务活跃且未提交的事务id的集合。
- min_trx_id:事务活跃且未提交的事务中最小事务id。
- max_trx_id:已经创建过的事务最大id的下一个id。
我们把事务状态划分为几类:
在这里,我们可以通过createor_trx_id获取当前事务ID,低水位即min_trx_id,高水位即max_trx_id。
Read View 通过记录高水位、低水位还有活跃未提交的事务集合,储存了不同事务的执行状态。
另外,我们还需要了解InnoDB表中的隐藏列:
trx_id:记录事务的id
roll_pointer:指向undo日志的指针,通过undo日志回滚计算出上一个id事务的行数据。
下面简单说明MVCC实现:
1.可重复读
按照可重复读的定义,一个事务启动的时候,能够看到所有已经提交的事务结果。但是之后,这个事务执行期间,其他事务的更新对它不可见。
可重复读隔离级别下,当事务开始时,会创建一个Read View(视图),视图的内容在上面有提到。
当执行select语句时,事务就回根据Read View里的内容,通过undo日志一直向前查找,直到找到第一个当前事务id之前的且已提交的事务。通过这种方式,在事务启动前的更新对于本事务是可见的,事务启动后的更新对于本事务是不可见的。也就实现了可重复读。
这是怎么实现的呢?InnoDB 为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务 ID。“活跃”指的就是,启动了但还没提交。
我们把事务状态划分为几类:
在这里,我们可以通过createor_trx_id获取当前事务ID,低水位即min_trx_id,高水位即max_trx_id。
这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)。而数据版本的可见性规则,就是基于每一个版本行的 trx_id 和这个一致性视图的对比结果得到的。
对于当前事务的启动瞬间来说,一个数据版本的 row trx_id,有以下几种可能:
-
如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的;
-
如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的;
-
如果落在黄色部分,那就包括两种情况
- 若 row trx_id 在数组中,表示这个版本是由还没提交的事务生成的,不可见;
- 若 row trx_id 不在数组中,表示这个版本是已经提交了的事务生成的,可见。
可重复读的核心就是一致性读(consistent read);而事务更新数据的时候,只能用当前读(后面会说明)。如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待。
2.读提交
读提交和可重复读类似,但是区别是创建Read View的时刻不同:
- 可重复读是在事务启动的时候创建Read View
- 读提交是每条select语句执行的时候,为每条select语句创建对应的Read View
判断版本可见还是不可见的规则,和可重复读一致。
这也意味着,事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务。
当前读
如果初始k = 1,有三个事务A、B、C,他们的事务执行过程是这样的:
按照可重复读规则,对于事务B,当前select读到的内容是k = 1。但是我们看到,在事务B之前,事务C就对k进行了更新操作。如果事务B对更新是读到的k为1,必然会导致数据的不一致。
所以,这里就用到了这样一条规则:更新数据都是先读后写的,而这个读,只能读当前的值,也就是C修改后的值(最后一次修改后的值),称为“当前读”(current read)。
除了 update 语句外,select 语句如果加锁,也是当前读。比如,如果把事务 A 的查询语句 select * from t where id=1 修改一下,加上 lock in share mode 或 for update,也都可以读到版本号是 101 的数据,返回的 k 的值是 3。
两阶段锁协议
还是上面的例子,如果事务C在事务B执行完更新语句后提交,那怎么办呢?
那么两阶段锁协议就排上用场了,两阶段锁协议是指在对行操作时会对该行进行加锁,当事务提交时才对该行进行解锁。
所以,如果遇到这种情况,事务B的更新语句会因为拿不到锁而被阻塞。这样就能保证事务b读到的是在该语句之前更新的最新的数据,也就能保证数据的一致了。
由于两阶段锁协议的特点,我们要尽量避免长事务。如果不可避免使用长事务,那就要将锁的影响降到最小,也就是尽量把产生互斥锁的语句放到靠后执行。
参考资料
-
极客时间 —— MySQL45讲 :事务隔离:为什么你改了我还看不见?
-
极客时间 —— MySQL45讲 :事务到底是隔离的还是不隔离的?
还没发表评论,快来发表第一个评论吧~