zhengsj 2020-03-28
如何通过单纯加锁实现RC隔离级别的隔离效果?
对InnoDB引擎下的mysql数据库支持行级锁,通过对事务访问时增加排他锁(X锁)可以防止其他事务的访问,只有在该事务锁提交也就是commit后才可以访问,避免脏读产生。但是在多读的场景下,一个事务假如在进行update操作,后面有许多请求都想要单纯进行读操作(普通的SELECT语句),可是因为有锁的存在只能进行等待。该方法在多读的并发环境下效率大大降低。
真实情况下由于InnoDB在RC和RR隔离级别下使用了MVCC机制,实现了一致性非阻塞读,提高了并发读写效率。其实读到的都是事务发生之前最新的快照版本。MVCC只是实现了快照读,对于当前读还是使用的排他锁(文章最后会总结不同索引条件下排他锁的情况)。
MVCC实现原理(仅对于RC和RR两种隔离级别有MVCC)
对于使用InnoDB存储引擎的表来说,它的聚簇索引记录中都会包含两个必要的隐藏列(row_id并不是必要的,如果我们创建的表中有主键或者非null唯一键都不会再创建row_id)
6字节的事务ID(DB_TRX_ID)每次对某条记录进行改动时,会把对应的事务ID值赋给DB_TRX_ID隐藏列(也就是每个行记录上这个字段会记录最近把这个字段修改的事务id)
7字节的回滚指针(DB_ROLL_PTR)每次对某条记录进行改进时,这个隐藏列会存一个指针,可以通过这个指针找到该记录修改前的信息
隐藏ID
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
在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,意味着当前事务在访问它自己 |
初始数据行。F1~F6是字段的名,1~6对应该字段的值。后面三个隐藏字段分别对应该行的事务号和回滚指针,假如这条数据是刚insert的,可以认为ID为1,其余两个字段为空。
Session1更改该行的各个字段的值,会进行如下图所作操作:
用排他锁(X锁)锁定改行
记录redo log
把改行修改前的值Copy到undo log
修改当前行的值,当前事务编号为01(示例中填写为01),使回滚指针指向undo log中修改前的行
Session2再来修改该行的值,与Session1中的步骤基本相同,但此时undo log中有两行记录,并且通过回滚指针连在一起
使用MVCC使得在读取数据的时候,Innodb几乎可以不用获得任何锁,每个查询都通过版本检查,只获得自己需要的数据版本,从而大大提高了系统的并发度。但是这种策略的缺点是每行记录都需要额外的存储空间,更多的检查工作和一个额外的维护工作。
但是Innodb并不是单纯的使用MVCC,只是在读操作时使用MVCC代替读锁,在进行其他操作依旧需要用到排他锁。
当事务仅仅修改一行记录的时候使用MVCC是可以的,但是当涉及多行记录的修改,MVCC就力不从心了。比如事务a执行理想的MVCC,想要修改两行数据,第一行修改成功,但是第二行修改失败,此时需要回滚第一行,但是以为第一行没有加行锁,事务b可能此时就对第二行进行了修改,但若后来事务a进行了回滚,那事务b进行的操作就会遭到破坏
所以Innodb只是借了MVCC这个名字,提供了非阻塞读。在写数据时依旧使用的排他锁(不使用间隙锁,所以依旧可以执行插入操作,所以不能避免幻读),其他事务依旧可以读取上锁的数据是因为读的只是镜像版本而已
6.RC隔离级别下行级锁的情况
在RC隔离级别下,行级锁并不是直接锁记录,而是会锁索引。索引又分为主键索引和唯一索引和非唯一索引。当sql语句操作主键索引时,mysql会直接在该主键索引上加排他锁;当sql语句操作唯一索引时,mysql会对该唯一索引记录加入排他锁,然后使用该条记录的主键通过主键索引(聚簇索引)在主键上加入排他锁;当sql语句操作非唯一索引,首先会将所有满足where条件的非唯一索引加排他锁,依然会通过聚簇索引对主键加锁;在无索引的情况下,因为无法直接定位到记录,所以直接走聚簇索引,走全表扫描,会将聚簇索引上所有的记录都加入排他锁(注意这里的并不是表锁)。mysql对之后的流程进行了优化,会将所有不满足条件的记录放锁,这样保证了最后只会有满足条件的记录上锁,降低了所冲突发生的概率,但这个流程是不会省略的。
7.RC隔离级别下的半一致性读
这是一种夹在普通读和锁定读之间的一种读取方式。它只在READ COMMITTED隔离级别下(或者在开启了innodb_locks_unsafe_for_binlog系统变量的情况下)使用update语句才会使用。具体的含义就是当UPDATE
语句读取已经被其他事务加了锁的记录时,InnoDB会将该记录的最新提交的版本读出来,然后判断该版本是否与update语句中的where条件相匹配,如果不匹配则不对该记录加锁,从而跳到下一条记录;如果匹配则再次读取该记录并对其进行加锁。这样子处理只是为了让update语句尽量少被别的语句阻塞。