MVCC和间隙读

InnoDB默认的隔离级别是RR(可重复读),可以解决脏读和不可重复读,但是不能解决幻读问题。
什么是幻读?
事务A读取了一个范围内的数据,此时事务B在该范围内插入了一条数据,并立马提交了事务,此时事务A再次读取这个范围的数据时,发现多了一条,就好像幻觉一样。

不可重复读
不可重复读是指在同一个事务内,两个相同的查询返回了不同的结果。
例如:事务T1读取某一数据,事务T2读取并修改了该数据,T1为了对读取值进行检验而再次读取该数据,便得到了不同的结果。
什么是MVCC?
多版本并发控制 。InnoDB为每行记录添加了一个版本号(系统版本号),每当修改数据时,版本号加一。
在读取事务开始时,系统会给事务一个当前版本号,事务会读取版本号<=当前版本号的数据,这时就算另一个事务插入一个数据,并立马提交,新插入这条数据的版本号会比读取事务的版本号高,因此读取事务读的数据还是不会变。
MVCC为查询提供了一个基于时间的点的快照。这个查询只能看到在自己之前提交的数据,而在查询开始之后提交的数据是不可以看到的。
在每行记录后面记录两个隐藏的列,一个记录创建时间,一个记录删除时间,记录的是版本号,这里可以理解为事物号。
MVCC的具体操作:
  • SELECT时,读取创建版本号<=当前事务版本号,删除版本号为空或>当前事务版本号。
  • INSERT时,保存当前事务版本号为行的创建版本号
  • DELETE时,保存当前事务版本号为行的删除版本号
  • UPDATE时,插入一条新纪录,保存当前事务版本号为行创建版本号,同时保存当前事务版本号到原来删除的行

例如:
此时books表中有5条数据,版本号为1
事务A,系统版本号2:select * from books;因为1<=2所以此时会读取5条数据。
事务B,系统版本号3:insert into books ...,插入一条数据,新插入的数据版本号为3,而其他的数据的版本号仍然是2,插入完成之后commit,事务结束。
事务A,系统版本号2:再次select * from books;只能读取<=2的数据,事务B新插入的那条数据版本号为3,因此读不出来,解决了幻读的问题。
在默认隔离级别REPEATABLE READ下,同一事务的所有一致性读只会读取第一次查询时创建的快照
从第一个select开始

我们且看,在RR级别中,通过MVCC机制,虽然让数据变得可重复读,但我们读到的数据可能是历史数据,是不及时的数据,不是数据库当前的数据!这在一些对于数据的时效特别敏感的业务中,就很可能出问题。
对于这种读取历史数据的方式,我们叫它快照读 (snapshot read),而读取数据库当前版本数据的方式,叫当前读 (current read)。很显然,在MVCC中:
  • 快照读:就是select
    • select * from table ....;
  • 当前读:特殊的读操作,插入/更新/删除操作,属于当前读,处理的都是当前的数据,需要加锁。
    • select * from table where ? lock in share mode;
    • select * from table where ? for update;
    • insert;
    • update ;
    • delete;
事务的隔离级别实际上都是定义了当前读的级别,MySQL为了减少锁处理(包括等待其它锁)的时间,提升并发能力,引入了快照读的概念,使得select不用加锁。而update、insert这些“当前读”,就需要另外的模块来解决了。

我们分两个方面说:
  1.快照读:对于快照读,其实是不会出现幻读问题的,通过上面我们得知,select时只会读取小于等于当前事务版本的行,但是新行的版本号是高于读事务的,那么新插入的行对之前的读事务是不可见的。
  2.当前读:因为当前读,读到的往往是最新的行数据,但是对于事务1更新了一行,同时事务2插入了一个新行(利用一个非唯一索引进行更新),那么会利用gap锁去控制新行的插入来避免这个问题。一个例子看一下:

我们可以看到,事务A在更新之后,事务B进行插入操作的时候会阻塞,但是这里使用的不是行锁,这就是因为rr隔离模式下,mysql使用的是next-keylocking机制防止“当前读”的幻读问题。如果不阻塞新插入的数据,那么就会导致更新之后,再次查询时会发现部分数据没有更新,本意是按照索引更新所有的行,但是新插入的行没有更新,这就会令我们很奇怪。

那需要先说说Mysql里面特殊的锁——Next-Key锁:
  Next-Key锁是行锁和Gap锁(间隙锁)的合体(可以理解为二者相加,因为gap锁是开区间的,加上行锁正好是闭区间)。间隙锁,顾名思义,是对一个间隙进行加锁,间隙是索引的间隙,也就是说, 更新的时候必须走索引,否则会将全表锁住。导致其他所有的写操作全部阻塞 。(如果使用的是没有索引的字段,(即使没有匹配到任何数据)',那么会给全表加入gap锁。同时,它不能像上文中行锁一样经过MySQL Server过滤自动解除不满足条件的锁,因为没有索引,则这些字段也就没有排序,也就没有区间。除非该事务提交,否则其它事务无法插入任何数据)next-key锁主要是针对非唯一索引,因为唯一索引和主键索引每次只会定位到单条记录,所以不需要next-key锁

当按照id(非唯一索引,不是主键,主键是name)进行更新或删除的时候会先对id索引进行加锁,但加的是next_key锁。因为在RR隔离级别下,需要防止“当前读”的幻读问题,加上next-keylock之后,在[6-10]区间和[10-11]区间进行插入时会阻塞,因为已经加了next-key锁,为什么用next-key锁?因为新增加的记录只能在10的左边和10的右边或者就是10。那么锁住范围后就能保证防止“幻读”。
如果使用的是没有索引的字段,(即使没有匹配到任何数据)',那么会给全表加入gap锁。同时,它不能像上文中行锁一样经过MySQL Server过滤自动解除不满足条件的锁,因为没有索引,则这些字段也就没有排序,也就没有区间。除非该事务提交,否则其它事务无法插入任何数据。



我的理解是针对于快照读:其实是不会出现幻读问题的,通过上面我们得知,select时只会读取小于等于当前事务版本的行,但是新行的版本号是高于读事务的,那么新插入的行对之前的读事务是不可见的。
而针对于当前读:因为当前读,读到的往往是最新的行数据,但是对于事务1更新了一行,同时事务2插入了一个新行(利用一个非唯一索引进行更新),那么就会导致更新之后,再次查询时(可能用 select * from xx for update 当前读 去查询数据)会发现部分数据没有更新,本意是按照索引更新所有的行,但是新插入的行没有更新,这就会令我们很奇怪。那么会利用gap锁去控制新行的插入来避免这个问题。
还有一种情况是:
这里A的前后两次读,均为快照读,而且是在同一个事务中。但是B先插入直接提交,此时A再update,update属于当前读,所以可以作用于新插入的行,并且将修改行的当前版本号设为A的事务号,所以第二次的快照读,是可以读取到的,因为同事务号。这种情况符合MVCC的规则,有了nextkey锁以后就不能再修改或者删除和插入了

innodb通过read-view避免一致性非锁定读的幻读.通过gap lock避免当前读(锁定读)时的幻读




RC和RR都是基于Mvcc实现,但是读取的快照数据是不同的。RC级别下,对于快照读,读取的总是最新的数据,也就出现了上面的例子,一个事务中两次读到了不同的结果。而RR级别总是读到小于等于此事务的数据,也就实现了可重读。


  • 7
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值