隔离级别与mvcc的那些事

数据库的四种隔离级别中,最常用的是read committed(rc)读已提交和repeatable read(rr)两种

在mysql中,上述两种隔离级别都依赖于多版本并发控制机制mvcc,本文阐述mysql通过mvcc实现rc/rr两种隔离级别的原理,并借此引出幻读以及间隙锁如何解决幻读

事务ID是在mysql开启事务时为其分配的递增序列号,由于是递增的,所以可以基于此判断事务先后关系

MVCC的多版本指的是针对数据库中的一行数据,都可能通过undolog中的数据算出多条行数据,每行数据版本不同(是为多版本),针对每次写操作,事务提交前,都会在undolog中记录相反的变动(是为回滚log),以及对应的事务ID,再结合数据表中的当前行数据,就可以回溯出一个行的的多个版本了

innodb会为每行数据添加两个字段 up_txid del_txid,分别是更新事务ID、删除事务ID,事务新增或者更新一个数据行后,会将该事务ID记录在该行数据的up_txid中,事务删除行数据后,会将该事务ID记录在del_txid中

举例说明下多版本到底是咋回事,表T(id,age)

1)事务A,事务ID=v1,执行insert into T (1,23);

T表中数据如下:

id,age,up_txid,del_txid

1,23,v1,null

undolog中新增一条数据如下:rowid,回滚语句,txid,pre_rowid

(行号/回滚语句/对应的事务ID/该行数据的前一条undolog行号)

10001,delete from T where id=1,v1,null

2)事务B,事务ID=v2,执行update T set age=age+1 where id=1;

T表中数据如下:

id,age,up_txid,del_txid

1,24,v2,null

undolog中新增一条数据如下:rowid,回滚语句,txid,pre_rowid

10002,update T set age=age-1 where id=1,v2,10001

3)事务C,事务ID=v3,执行delete from T where id=1;

 T表中数据如下:

id,age,up_txid,del_txid

1,24,v2,v3

undolog中新增一条数据如下:rowid,回滚语句,txid,pre_rowid

10003,insert into T (1,23),v3,10002

好了,现在id=1的这行数据先后被三个事务操作过,并在undolog中生成了3条回滚数据,此时,我们结合表中行数据+undolog数据,回溯出的多行数据从新往旧就是这样子的:

最新的是当前表中数据,最老的是初始状态,没有该行数据

现在,多版本数据已经有了,在此基础上,在read repeatable隔离级别下,查询是如何工作的呢?

该隔离级别下的事务启动时,除了分配上面说的事务ID外,系统还会查出当前活跃的事务ID列表(也就是开启了但还未提交的事务),分配给该事务存储下来,有了这些信息,就可以实现快照读了,RR隔离级别下,其查询到的行数据需要满足:

1.行数据的up_txid<=当前事务ID,并且不在活跃事务ID列表中

2.行数据的del_txid为null,或者>当前事务ID,或者在活跃事务ID列表中

简单理解下,只查询在当前事务开启之前就已经提交的数据,并且这行数据未被删除或者在当前事务开启后删除,相当于事务启动时,拍了个快照,事务执行期间,就通过这个快照读取数据,其他事务的变动不会再对当前事务产生影响,是为可重复读

在读取时,会从最新的一条数据开始读起,如果满足条件就以其为准,如果不满足就找到更旧的一行数据继续判断

看起来,上面这种方式是可以避免幻读的,我们需要对幻读做一个准确说明:

1.幻读仅仅指读到了其他事务新插入的行,如果是修改过来的,则不算幻读

2.上面说了, 在RR隔离级别下,普通的查询是快照读,是不会看到别的事务插⼊的数据的。因此,幻读只在 当前读 下才会出现
 

所以我们可以这样定义下,RR隔离级别下,可以保证快照读不存在幻读的情况,这个保证是由MVCC提供的。那当前读会出现幻读吗?也不会,怎么做到的,下文会有介绍

read committed隔离级别下,查询又是如何工作的呢?

        和RR隔离级别一样的是,RC隔离级别下的查询也是快照读,区别就是RC隔离级别下每次select时都会获取下当前活跃事务ID列表,然后从最新一行数据开始,判断是否满足如下条件,不满足则继续判断更旧的一行数据:

1.行数据的up_txid不在活跃事务ID列表中,表示已经提交

2.行数据的del_txid为null,或者在活跃事务ID列表中未提交

简单理解下,就是每次都读取当前已经提交的并且未被删除的最新数据,相当于每次查询都会拍个快照

 

有两点要特别说明下:

1.在上面介绍MVCC时,说的是一行数据可能存在多个版本,那也可能只有一个版本,undolog中的数据不会一直存在,当mysql检测到某条数据不会再被其他事务读取时,就会删掉它,那mysql是怎么判断的呢?基于事务ID,比如undolog中存了v7这条回滚数据,而现在最小的活跃事务ID是v8,那v7这条数据就不可能再被读到,就可以删除了。

2.在介绍select查询时,特别注明了是未加锁的查询,普通的select语句就是未加锁的,那如果是加锁的查询呢?比如  

select k from t where id=1 lock in share mode;
 
select k from t where id=1 for update;
 
除此之外,在update/delete时,也会在加锁后先查询再写库,区别就是update/delete语句的查询前会自动加锁,比如update t set age=age+1 where id=1,会首先加锁,然后读取数据,更新,再释放锁

如果查询加了锁,就不在mvcc的控制范畴了,因为此时用的是当前读 ,当前读的规则,就是要能读到所有已经提交的记录的最新值。当前读是由锁来保证的,大家都知道,innodb中有行锁,上面举例的几条语句,都会锁住id=1的这行数据,这样其他事务如果要对id=1这行数据进行当前读,只能等行锁释放,等到啥时候?事务完成的时候会释放掉锁,既然事务都完成了,那其他事务自然能读取到已提交的最新值。

这就够了吗?记得我们上面说的,当前读情况下会存在幻读,行锁只能锁住已经存在的行,幻读读的是其他事务新增的行,这是行锁不能避免的。为了解决幻读问题,innodb只好引入新的锁,也就是间隙锁(Gap Lock),注意,间隙锁只在RR隔离级别下存在,我们通过一个例子来说明下间隙锁是如何解决幻读问题的:

CREATE TABLE `t` (
`id` int(11) NOT NULL,
`c` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
) ENGINE=InnoDB;
insert into t values(0,0),(5,5), (10,10),(15,15), (20,20),(25,25);
注意:我们的前提是在RR隔离级别下

这时候,t表的主键索引上就产生了6条记录加七个间隙

现在sessionA执行select * from t where id>1 and id<=5 for update;innodb会加上id=5这行的行锁,以及(0,5)这个间隙的间隙锁,这时候如果sessionB执行insert into t values(3,3,3),就会被间隙锁挡住,等待sessionA事务提交后才能继续执行。因此在sessionA的事务运行过程中,即使多次执行select * from t where id>1 and id<=5 for update;也不会读到id=5以外的值,这就是间隙锁的作用。

现在你应该理解了,RR隔离级别下,结合行锁+间隙锁,可以保证当前读不存在幻读的情况,再结合前面的定义:RR隔离级别下,可以保证快照读不存在幻读的情况,我们就知道了,RR隔离级别下,可以保证不存在幻读的情况

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值