InnoDB在RR隔离级别下解决幻读问题

表象:快照读(非阻塞读)—伪MVCC
内在:next-key锁(行锁+gap锁)

当前读和快照读

当前读:select…lock in share mode(共享锁),select…for update(排他锁)
当前读:update,delete,insert(排他锁)

当前读就是加了锁的增删改查语句,无论是上的共享锁还是排他锁均为当前读,读取的为当前的最新版本并且读取之后还要保证其他并发事务不能修改当前记录,对读取的记录加锁RDBMS主要由两部分组成(程序实例和存储(InnoDB))

以update语句为例:当update SQL发给Mysql之后,MysqlSever会根据where条件,读取第一条满足条件的记录(select row 1)innoDB引擎会将第一条记录返回并加锁(return&lock)待mysqlsever接收到这条加锁的记录后会发起一个update操作去更新这条记录,一条记录更新完成了之后再读取下一条记录直至没有满足条件的记录为止,update操作就包含了一个当前读来获取数据的最新版本,就跟在readcommitted下出现的这个幻读的情况一样由于先前另外一个事务新提交了一个数据当前事务update全表的时候就莫名多了一条数据即读取到了数据的最新版本,同理DELETE操作也一样,insert操作会稍有不同,简单的来说insert操作可能会触发唯一键的冲突检查也会进行一个当前读

快照读:不加锁的非阻塞读,select(不加锁的条件是以在事务隔离级别不为Serializable的前提下才成立的,由于serializable是串行读,所以此时的快照读也退化为当前读,即select…lock in share mode 模式,之所以出现快照读是基于提升并发效率的考虑,快照读的实现是基于多版本的并发控制即MVCC,可以认为MVCC是行级锁的一个变种,但是它在很多情况下避免了加锁的操作,因此开销更低,既然是基于多版本,也就意味着快照读可能读到的并不是最新版本的数据,可能是之前的历史版本)

InnoDB的非阻塞读是在RC/RR隔离级别下是如何实现的

三个因子
1.数据行里的
DB_TRX_ID(标识最近一次对本行数据做修改,无论是insert,update,事务的标识符,即最后一次修改本行数据的事务ID,delete在innoDB看来也是一次update操作,更新行中的一个特殊位,将行标识为deleted,也就是说数据行中除了这三列,还有一个被称为deleted的隐藏列)
DB_ROLL_PTR(回滚指针,指写入回滚段ROLLBACK SEGMENT的undo日志记录,如果一行记录被更新,则undolockrecalled包含从建该行记录被更新之前内容所必须的信息)
DB_ROW_ID(行号,包含一个随着新行插入而单调递增的行ID,当由innoDB自动产生具体索引时,具体索引会包括这个行id的值,否则这个行ID不会出现在任何索引中)字段

2.undo日志:当我们对记录做了变更操作时,就会产生undo记录,undo记录中存储的是老版的数据,当一个旧的事务需要读取数据时,为了能读取到老版本的数据,需要顺着undo链找到满足其可见性的记录,undolog主要分为两种insertundolog,updateundolog,其中insertundolog表示事务对insert新记录产生的undolog,只在事务回滚时需要并且在事务提交后就可能会立即丢弃。updateundolog事务对记录进行delete或者update时产生的undolog,不仅在事务回滚时需要,快照读也需要所以不能删除,只有数据库所使用的快照中不涉及该日志记录对应的回滚日志才会被删除

3.read view:主要用来做可见性判断的,即当我们做快照读select的时候会针对我们所查询的数据创建出一个read view来决定当前事务能看到的是哪个版本的数据,有可能是当前最新的数据,也有可能只允许你看undolog里面某个版本的数据,遵循可见性算法。主要是将要修改的数据的DB_TRX_ID取出来,与系统其他活跃ID做对比如果大于或者等于这些ID的话,就通过DB_ROLL_PTR指针去取出undolog上一层的DB_TRX_ID直到小于这些活跃事务的ID为止,这样就保证了我们获取的数据版本是是当前最稳定的版本

每当我们start transaction的时候事务ID都会去递增,也就是说越新开启的事务这个ID就会越大由于生成是时机不同,造成了RC,RR两种事务隔离级别的可见性不同

在Repeatable read隔离级别下,session Strat transaction后的第一条快照读会创建一个快照即read view ,将当前活跃的其他事务记录起来,此后再调用快照读的时候还是用的是同一个read view

在 read committed级别下,事务中每条 select语句每次调用快照读的时候都会创建新的快照,这就是为什么我们在此隔离级别下能用快照读看到其他事务已提交的对表的增删改了,而在RR下如果首次使用快照读是在别的事务对数据进行增删改提交之前的,此后即便别的事务对数据做了增删改并提交,还是读不到数据变更的原因(首次select的时机很重要)

由于以上的三个因子才使得innoDB在RR或者RC级别下支持非阻塞读,而读取数据时的非阻塞就是所谓的MVCC(Multi-Version Concurrency Control多版本控制),而InnoDB的非阻塞读机制实现的仿制版的MVCC,并没有实现MVCC的核心的多版本共存,undolog中的内容只是串行化的结果,记录了多个事务的过程,不属于多版本共存读不加锁,读写不冲突,在读多写少的应用中读写不冲突是非常重要的,极大的增加了系统的并发性能,快照读并非是幻读现象发生的根本,只是你如果先要提交数据变更的事务,打开read view时不论别的事务的变更是否已提交,在当前事务内再次调用快照读的时候还是读的可见性版本内的数据,有一种掩耳盗铃的意思在里面。而真正防止幻读发生的原因是事务对数据加了next-key锁。

next-key锁

1.行锁:是对单独行记录上的锁,

2.gap锁:Gap是索引树中插入新记录的空隙,而gaplock间隙锁即锁定一个范围但不包括记录本身,gap锁的目的是为了防止事务的两次当前读出现幻读的情况gap在RC隔离级别或者更低的隔离级别下是没有的,这就是RC等隔离级别无法避免幻读的原因,而在RR以及serializable级别下默认都支持gap锁

对主键索引或者唯一索引会引用Gap锁吗

视情况而定
1.如果where条件全部命中,则不会使用gap锁,只会加记录锁(精确查询所有记录都有例如 select * from table where id in(1,5,7) 157数据均在此table存在并且出现,就是全部命中,如果只查到了部分,则为部分命中)

假设id为主键或者唯一键,那么在事务A中我们将id作为where筛选条件去做当前读的时候,比如delete from table where id = 9 ,此时事务B新增了一条记录,必然也会出现在这个当前读的范围之外,所以在事务B新增数据并提交了之后事务A再去做当前读还是获取到原来的数据集,并不会产生所谓的幻读现象,所以此时加行锁就足够了,锁住这写ID唯一的特定行便可以防止另外的事务对该结果集做出的影响。也就是说没有加gap锁的必要,值得注意的是,加锁的时候如果我们走的是主键之外的索引,那么我们需要对当前索引,以及主键索引上对应的记录都上锁,

2.如果where条件部分命中或者全不命中,则会加Gap锁

gap锁会用在非唯一索引或者不走索引的当前读中

当前读:1.非唯一索引(使用Gap防止幻读的发生)

例如select … from tabel where id = 8 (tabel中有两条满足条件的数据,name为primary key,id key),在执行词语句的过程中未提交,另一个事务插入了一条id为8的其他数据,此时当前读的数据则会变为三条,这样就会发生幻读,此时我们就要引入gap锁。(具体详见官方文档 左开右闭区间,在这些区间内一旦上了gap锁,该区间就无法插入数据了),因此gap锁是为了防止插入的,对于普通非唯一索引并不是所有的gap都会去上锁,只会对要修改的地方的周边上gap锁。主键的值也起到了一定的作用

2.不走索引(当当前读不走索引的时候,他对所有的gap都上锁,也就类似锁表,可以达到防止幻读的效果,相比表锁,这种上锁的方式代价更大,需要避免)

innoDB的RR级别主要通过引入next-key锁来避免幻读问题,而next-key由recorelock和gaplock组成,gaplock会用在非唯一索引或者不走索引的当前读以及仅命中检索条件的部分结果集,并且用到主键索引,以及唯一索引的当前读中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章