锁模块之当前读和快照读

锁模块之当前读和快照读

InnoDB可重复读隔离级别下是如何避免幻读的。

这里我们分为表象和内在

  • 表象:快照读(非阻塞读)——伪MVCC (前提是在RR级别下)
  • 内在:next-key 锁(行锁+gap锁)

当前读和快照读

  • 当前读:select … lock in share mode,select … for update
  • 当前读2:update ,delete, insert

其实当前读就是加了锁的增删改查语句,不管你上的是排他锁还是共享锁都是当前读。那为什么是当前读呢?原因是它读取了记录的最新版本,并且还要保证读取之后还需要保证其他并发事物不能修改当前记录,对读取的事物加锁,其中除了select … lock in share mode它会加共享锁之外其它的语句都会加上排它锁。

那为什么update ,delete, insert也是当前读呢?咋们了解到RDDMS主要由两大部分组成,程序实例还有存储。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-03SSxKzs-1632115156134)(C:\Users\黄福荣\Desktop\md\ms图库\RDDMS组成.png)]

程序实例在这里指的是mysqlServer 的实例,存储便是InnoDB我们拿update table set ? where ?;来举个例子当update Sql发送给mysql之后呢,mysql的sqlServer会根据where条件读取第一天满足条件的记录selcted row1然后InnoDB引擎会降低一条记录返回并加上锁 return & lock 待mysql的sql Server 接收到这条加锁的记录后会发起一个update row 的操作,去更新这条记录,一条记录更新完成之后呢在更新下一条记录直至没有满足条件的记录为止。

update操作内部就包含了一个当前读来获取数据的最新版本,这跟咱们之前在read-commited下出现的幻读的情况一样,由于先前一个事物新提交了一条数据,当前事物update 全表的时候就莫名其妙的多了一条数据,即读取到了数据的最新版本同样delete操作也一样,insert操作稍有些不同。简单的来说就是insert操作可能会触发唯一键的冲突检查,也会进行一个当前读。

而快照和当前读不一样它就是一个简单的select操作不加锁,不加锁的条件是在事物隔离级别不再serializable级别下前提下才成立的。在serializable由于是串行读,所以此时的快照读也会退还成当前读,即 select … lock in share mode 模式。

之所以出现快照读是基于提升并发性能的考虑,快照读的实现是基于多版本并发控制即MVCC,可以MVCC是行级锁的一个变种,但是它在很多情况下避免了加锁操作,因此开销更低,既然是基于多版本,也就是有可能快照读读到的并不是数据的最新版本,可能是之前的历史版本。

咱们来看看实际的例子。

我们打开4个session其中两个是RC,两个是RR

session1和session2是RC

#我们先将session1和session2开启事务
start transaction;

#session1
select * from account_innodb where id = 2; #结果 name cindy balance 1000

#session2对其进行更新
update account_innodb set balance = 600 where id = 2;
#更新后提交事物
commit;

#sseion1 分别用当前读和快照读来做查询操作
#快照读
select * from account_innodb where id = 2; # 结果 600
#当前读
select * from account_innodb where id = 2 lock in share mode; # 结果 600

#小结:在rC隔离级别下当前读和快照读的结果是一样的。

#session3和session4是RR隔离级别可重复读

#我们先将session3和session4开启事务
start transaction;

#session3
select * from account_innodb where id = 2; #结果 name cindy balance 600

#session4对其进行更新
update account_innodb set balance = 300 where id = 2;
#更新后提交事物
commit;

#sseion3 分别用当前读和快照读来做查询操作
#当前读
select * from account_innodb where id = 2 lock in share mode; # 结果 300
#快照读
select * from account_innodb where id = 2; # 结果 600

#快照读返回的数据版本和没修改之前的数据版本是一致的,而当前读依旧返回的是数据的最新版本。也就是说在RR隔离级别下快照读有可能读到数据的历史版本。
#快照读读到最新数据:我们先不在session3用快照读读取数据,直接在session4更新数据,接着再在session3里读取数据,就可以读到最新数据。
#也就是说在RR事物隔离级别下事务首次调用快照读的地方很关键,也就是创建数据的时机决定了读取数据的版本。

RC,RR级别下的InnoDB的非阻塞读(快照读)如何实现。

InnoDB要实现RC和RR级别下的快照读离不开下面几个条件:

1.数据行里db_trx_id,db_roll_ptr,bd_row_id字段

每行数据除了存储数据外还要存储一些字段,其中最关键的是上面这几个字段,

其中db_trx_id顾名思义就是跟事物相关的,该字段用来标识最近一次对本行记录做修改不管不insert还是update,至于delete操作在InnoDB看来也只是一次update操作,更新行中的一个特殊位,将行表示为delete也就是说数据行里面除了上面的三列,还有别的隐藏列,删除的就是deleted这个列。

db_roll_ptr roll-rollback ,ptr-pointer即回滚指针。指写入回滚段rollback-sagement的ando日志记录,如果一行记录被更新则anuo-roll-report包含同件改行记录被更新之前内容所必须的信息。

db_row_id 即行号包含一个随着新行插入而单调递增的行id,当由InnoDB自动产生聚集索引时,聚集索引会包括行id的值。否则这个行id不会出现在任何一个行索引中。

2.ando日志

当我们对记录做了变更操作时,就会产生ando记录,ando记录中存储的是老版数据,当一个旧的事务需要读取数据时,为了能读取到老版本的数据,需要顺着ando链找到其可见性的记录。

ando_log主要分为两种inser_ando_log以及update_ando_log其中insert_ando_log表示的是事务对insert新记录产生的ando_log,在事务回滚时需要并且在事务提交后就可以立即丢弃。

update_ando_log是指对数据进行update或者delete产生的ando_log不仅在事务回滚时需要也在快照读需要,所以不能随便删除,只有当数据库所使用的快照中不涉及该日志记录,对应得回滚记录才会被perg线程删除。

日志得工作方式大概时什么样子得呢?咋们来看看简版得演示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4ZvcYyIO-1632115156141)(C:\Users\黄福荣\Desktop\md\ms图库\日志工作流程.png)]

演示的是事务对行记录的更新过程,InnoDB在内部做了很多的内部工作,这里我们不细说。

我们要对db_row_id为1的行去做变动,这行被事务A做修改将原来Field2里面的值从12改成了32。修改的流程将会是这个样子的首先用排它锁锁定该行,随后将改行修改之前的值拷贝一份到ando_log里面,然后修改当前行的值然后修改事务id db_trx_id 使用回滚指针 db_roll_prt 指向ando_log中的修改之前的行。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6qM7pdKX-1632115156148)(C:\Users\黄福荣\Desktop\md\ms图库\andolog流程2.png)]

在这之后呢假如数据库还有别的事物,再用快照记录来读取该日志记录,那么对用的ando_log还没有被清除,此事某个事物又对同一行记录做修改,将Field3从13该成45,那么这行数据又多了一条ando_log记录。

以上就是ando日志的大概样子了,他按照修改时间的顺序从近到远,通过db_roll_ptr给连接起来了。

现在有了行隐藏列有了ando日志,完事具备只欠东风。这个东风便是read view

3.read view

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kvalxRuo-1632115156151)(C:\Users\黄福荣\Desktop\md\ms图库\mysql源码.png)]

我们来看看mysql源码,这里由两个字段一个是 m_low_limit_id 即表示的是活动事务中的最大的id,另外一个m_up_limit_id 存的是活动事务范围内最小的id。每当我们start transaction 的时候我们的事务id都会去递增。也就是说最新开启的事务id是越大的

我们就m_low_limit_id ,m_up_limit_id 根据这两个字段与dd_trx_id做对比进而决定让它是不是去回溯到我们的ando_log去取出适应该版本的一个数据的版本来。

正是因为生成时期的不同造成了RC,RR存储隔离级的不同可见性。在repeatable级别下,session再start transaction 之后的第一条快照读,会创建一个快照即read view 将当前系统中活跃的其他事务记录起来,此后在调用快照读的时候,还是用的是同一个read view。而在read commited级别下事务中每条select语句也就是每次调用快照读的时候,都会创建一个新的快照。这就是我们之前在RC下能用快照读看到别的事物已提交的对表记录的增删改,而在RR下如果首次使用快照读,是在别的事务在对增删改进行提交之前,此后即便别的事物做了增删改并且做了提交还是读不到数据变更的原因。对于RR来讲首次事务select 的时机是相当重要的。咋们之前的例子也体现过了。

正因为以上的3个因子,才使得InnoDB在RR或者RC级别支持非阻塞读,而读取数据时的非阻塞是所谓的MVCC,而InnoDB得非阻塞读机制实现了仿造版的MVCC。MVCC代表多版本并发控制,读不加锁,读且读重读,在读多写少的OLTP应用中读且读重读是非常重要的,极大的增加了系统的兵法性能。那为什么这里仅实现了伪MVCC机制呢?因为并没有实现核心的多版本共存,ando_log中的内容只是串行化的结果记录了多个事务的过程,不属于多版本共存。

next-key锁

next-key锁回到RR级别下如何避免幻读这个问题,前面我们了解到在RR隔离级别下,具备让我们看不到ramdom行的能力,但这并不代表快照读是避免幻读的现象发生的根本。只是你先要提交数据变更的事物,打开read view时无论别的事务的变更是已提交,在当前事务内再次调用快照读的时候,还是读的是可见性版本内的数据。有一种掩耳盗铃的意味在里面。其实在RR(可重读读)的串行化隔离级别下,真正防止幻读发生的是事务读数据加了next-key锁

next-key 锁由两部分来组成

  • 行锁 (对单个行记录上的锁)
  • Gap锁

Gap是索引树中插入新记录的空隙,而gap lock 间隙锁即锁定一个范围但不包括记录本身,gap锁的目的是避免两个事物的同一当前读,出现幻读的情况,因此我们抓重点讨论加gap锁的情况

gap锁再read commited 以及更低级别的事物隔离级别下面是没有的,所以这就是RC 即读未提交隔离级别下无法避免幻读的原因,而在RR以及串行化隔离级别下默认都支持gap锁

这里我们主要讨论RR下Gap主要出现的场景:

在RR下不论删,改,查当前读若用到主键或者唯一键会用到gap锁吗?视情况而定,

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

我们来补充一下加锁的一些具体情况,

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kb6PY6vb-1632115156155)(C:\Users\黄福荣\Desktop\md\ms图库\加锁情况.png)]

如图所示表里的两个字段一个是name 是该表的主键,另外一个是id是该表的唯一键,如果我们执行 delete from table where id =9;该如何进行加锁呢?此时由于id是唯一索引,因此delete 语句会选择走id为条件的进行where条件的过滤,在找到id=9的这条记录之后首先会将这行数据加上行锁 Record lock,之后会根据读到的这个name列为主键索引就是我们的密集索引然后呢会根据这个密集索引name=d对应的主键索引下也加上一个Record lock 也就是加上一个排它锁。为什么密集索引上的记录也要加上排它锁呢?试想一下如果一个并发的sql是通过主键索引来更新的update table1 set id = 90;将这个id从9改成90,where id = d 此时如果delete 语句没有将主键索引的上的这条记录加锁,那么并发的这个update就会感知不到delete 语句的存在,而他们操作的是同一条记录。这样就违背了同一条记录delete和update要串行执行的约束。

  • 如果where条件部分命中或者全部命中,则会加Gap锁。

gap出现场景:主要出现在非唯一索引或者不走索引的当前读当中。

  • 非唯一索引

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-drzGgOZu-1632115156157)(C:\Users\黄福荣\Desktop\md\ms图库\情况1.png)]

如图所示innodb表tb1有两列,主键name和非唯一普通键id大家可以看到其中两行数据的id均为9。此时就需要用gap来防止幻读的发生了。大家可以试想一下如果我们的事物A第一次用当前读。选出id为9的数据delete from tb1 where id =9;如果只锁住选出来的两行,那么此后另外一个事物D,插入了id同样为9的数据,并提交。事物A再次用当前读选出id为9的数据时会取出3条这样呢就会发生幻读。因此这个时候我们就要引入gap锁,gap具体能在什么地方添加呢?我们来看看那个地方算做事gap。从图中可以看到gap地方跟我们走的 非唯一索引的值的分部有着很大的关系。都是一个左开右闭的区间(-∞,2],在这些区间内一旦上了锁该区间就没办法插入数据了。因此gap是用来防止插入的。对于普通非唯一索引来说呢并不是所有的gap都会去上锁。只会对要修改地方的周边去上gap锁。

  • 不走索引

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T0PGS18q-1632115156159)(C:\Users\黄福荣\Desktop\md\ms图库\不走索引.png)]

当当前读不走索引的时候,他就会对所有的gap都上锁,这里就类似于锁表了。这样也同时能达到幻读的效果。如图所示innodb表tb2有两列主键name和没有添加任何索引的id。此时我们用当前读即删除id=9的列。那么该表的所有gap均会被锁住。

走索引

[外链图片转存中…(img-T0PGS18q-1632115156159)]

当当前读不走索引的时候,他就会对所有的gap都上锁,这里就类似于锁表了。这样也同时能达到幻读的效果。如图所示innodb表tb2有两列主键name和没有添加任何索引的id。此时我们用当前读即删除id=9的列。那么该表的所有gap均会被锁住。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值