如何通过单纯加锁实现RC隔离级别的隔离效果?
对InnoDB引擎下的mysql数据库支持行级锁,通过对事务访问时增加排他锁(X锁)可以防止其他事务的访问,只有在该事务锁提交也就是commit后才可以访问,避免脏读产生。但是在多读的场景下,一个事务假如在进行update操作,后面有许多请求都想要单纯进行读操作,可是因为有锁的存在只能进行等待。该方法在多读的并发环境下效率大大降低。
真实情况下由于InnoDB在RC和RR隔离级别下使用了MVCC机制,实现了一致性非阻塞读,提高了并发读写效率。其实读到的都是事务发生之前最新的快照版本。
MVCC实现原理(仅对于RC和RR两种隔离级别有MVCC)
1.版本链
对于使用InnoDB存储引擎的表来说,它的聚簇索引记录中都会包含两个必要的隐藏列(row_id并不是必要的,如果我们创建的表中有主键或者非null唯一键都不会再创建row_id)
-
6字节的事务ID(DB_TRX_ID)每次对某条记录进行改动时,会把对应的事务ID值赋给DB_TRX_ID隐藏列(也就是每个行记录上这个字段会记录最近把这个字段修改的事务id)
-
7字节的回滚指针(DB_ROLL_PTR)每次对某条记录进行改进时,这个隐藏列会存一个指针,可以通过这个指针找到该记录修改前的信息
-
隐藏ID
2.MVCC实现的依赖:
-
undo log :undo log记录的时数据表记录行的多个版本,也就是事务执行过程的回滚段,其实就是MVCC中一行原始数据的多个版本镜像数据。
-
read view:主要用来判断当前版本数据的可见性
-
创建readvew的时机:在该隔离级别下(RC),每次发起SELECT都会创建readview;在RR隔离级别下,事务中的第一个SELECT请求才会开始创建readview。正是由于创建readview时机不同,决定了RC隔离级别可以避免脏读,但不能避免不可重复读。
read view中包含四个重要的内容
-
m_ids:在生成readview时当前系统系统中活跃的读写事务的事务id列表【当前事务(新建事务)与正在内存中commit的事务不在活跃事务列表中】
-
min_trx_id:表示在生成readview时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值
-
max_trx_id:表示生成readview时系统中应该分配给下一个事务的id值
-
creator_trx_id:表示生成readview的事务的事务id
需要注意的是:max_trx_id并不是m_ids中的最大值,事务id是递增分配的。比如现在id为1,2,3这三个事务,之后id为3的事务提交了。事务在生成readview时,m_ids就包括1,2,其中min_trx_id的值就是1,max_trx_id的值就是4
-
3.当前事务读取某个行记录时的具体过程(注意:当前事务与正在内存中commit的事务不在readview活跃列表中):
在innodb中,在RC隔离级别下创建新事务并每次执行SELECT语句后,innodb会将当前系统中活跃事务的列表创建一个副本(read view),副本中保存的是系统当前不应该被本事务看到的其他事务id列表。当前一个用户创建了一个事务,当他想要读取这个行记录的时候,innodb会将这个行记录当前的版本号与readview进行比较,比较过程具体如下:
当有了ReadView后,每次有某条事务想要访问某行记录时,只需要按照下边的步骤判断当前该行记录的版本是否可见 |
---|
1.如果该行的DB_TRX_ID属性值 < ReadView中min_trx_id的话,表明生成这个版本记录的事务已经在本次事务创建之前就已经提交了,所以该行记录的当前值是可见的。跳到步骤5. |
2.如果该行的DB_TRX_ID属性值 >= ReadView中max_trx_id(前面已经提到,这个并不是活跃事务列表中的最大值)的话,表明生成这个版本记录的事务在本次事务readview创建之后才开启,所以当前值不可见,跳到步骤4. |
3.如果ReadView中min_trx_id <= DB_TRX_ID属性值 <= ReadView中max_trx_id,那么就需要进行一次判断,如果DB_TRX_ID在事务活跃列表中,不可见,返回4,如果不在事务活跃列表中,说明生成这个版本的事务已经提交了,可以读,返回5. |
4.从该行记录的DB_ROLL_PTR指针所指向的回滚段中取出最新的undo-log的版本号,将它赋值该DB_TRX_ID,然后跳到步骤1. |
5.将该可见行的值返回。 |
6.如果被访问的事务id等于readview中的creator_trx_id,意味着当前事务在访问它自己 |
4.行的更新过程
-
初始数据行。F1~F6是字段的名,1~6对应该字段的值。后面三个隐藏字段分别对应该行的事务号和回滚指针,假如这条数据是刚insert的,可以认为ID为1,其余两个字段为空。
-
Session1更改该行的各个字段的值,会进行如下图所作操作:
用排他锁(X锁)锁定改行
记录redo log
把改行修改前的值Copy到undo log
修改当前行的值,当前事务编号为01(示例中填写为01),使回滚指针指向undo log中修改前的行
-
Session2再来修改该行的值,与Session1中的步骤基本相同,但此时undo log中有两行记录,并且通过回滚指针连在一起
5.InnoDB的MVCC多版本并发控制
使用MVCC使得在读取数据的时候,Innodb几乎可以不用获得任何锁,每个查询都通过版本检查,只获得自己需要的数据版本,从而大大提高了系统的并发度。但是这种策略的缺点是每行记录都需要额外的存储空间,更多的检查工作和一个额外的维护工作。
但是Innodb并不是单纯的使用MVCC,只是在读操作时使用MVCC代替读锁,在进行其他操作依旧需要用到排他锁。
当事务仅仅修改一行记录的时候使用MVCC是可以的,但是当涉及多行记录的修改,MVCC就力不从心了。比如事务a执行理想的MVCC,想要修改两行数据,第一行修改成功,但是第二行修改失败,此时需要回滚第一行,但是以为第一行没有加行锁,事务b可能此时就对第二行进行了修改,但若后来事务a进行了回滚,那事务b进行的操作就会遭到破坏
所以Innodb只是借了MVCC这个名字,提供了非阻塞读。在写数据时依旧使用的排他锁(不使用间隙锁,所以依旧可以执行插入操作,所以不能避免幻读),其他事务依旧可以读取上锁的数据是因为读的只是镜像版本而已