InnoDB的MVCC原理

转载自:https://blog.csdn.net/huaishu/article/details/89924250
    https://blog.csdn.net/fighterandknight/article/details/125197574?spm=1001.2014.3001.5502
    https://www.jianshu.com/p/d9bdf90005cb

什么是MVVC

  MVCC (Multi-Version Concurrency Control) 是一种基于多版本的并发控制协议,只有在InnoDB引擎下存在。MVCC是为了实现事务的隔离性,通过版本号,避免同一数据在不同事务间的竞争,可以把它当成基于多版本号的一种乐观锁。当然,这种乐观锁只在事务级别未提交锁和已提交锁时才会生效。MVCC最大的好处,读不加锁,读写不冲突。在读多写少的OLTP应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能。

  可以认为MVCC是行级锁的一个变种,但是它在很多情况下避免了加锁操作,因此开销更低。虽然实现机制有所不同,但大都实现了非阻塞的读操作,写操作也只锁定必要的行。

  MVCC只在 READ COMMITTED 和 REPEATABLE READ 两个隔离级别下工作。

MVVC的实现机制

  InnoDB存储引擎在每行数据都增加三个隐藏字段,一个唯一行号,一个记录创建的版本号,一个记录回滚的版本号。

  • 6字节的事务ID(DB_TRX_ID)字段:用来标识最近一次对本行记录做修改(insert|update)的事务的标识符,即最后一次修改本行记录的事务id。至于delete操作,在innodb看来也不过是一次update操作,更新行中的一个特殊位将行表示为deleted,并非真正删除。
  • 7字节的回滚指针(DB_ROLL_PTR)字段:指写入回滚段(rollback segment)的 undo log record (撤销日志记录记录)。如果一行记录被更新, 则 undo log record 包含 ‘重建该行记录被更新之前内容’ 所必须的信息。
  • 6字节的DB_ROW_ID字段:包含一个随着新行插入而单调递增的行ID,当由innodb自动产生聚集索引时,聚集索引会包括这个行ID的值,否则这个行ID不会出现在任何索引中。

  注意:执行删除SQL时,并不是直接将记录从数据页中抹掉,而是通过一个删除位(delete_flag)来进行标识,将该字段置为1即标识这行数据已经被删除了;同时和其他写一样会记录操作事务的trx_id。

  在多版本并发控制中,为了保证数据操作在多线程过程中,保证事务隔离的机制,降低锁竞争的压力,保证较高的并发量。
  在每开启一个事务时,会生成一个事务的版本号,被操作的数据会生成一条新的数据行(临时),但是在提交前对其他事务是不可见的,对于数据的更新(包括增删改)操作成功,会将这个版本号更新到数据的行中,事务提交成功,将新的版本号更新到此数据行中,这样保证了每个事务操作的数据,都是互不影响的,也不存在锁的问题。

上面的例子,当事务T2更改该行的值时,会进行如下操作:

用排他锁锁定该行
记录redo log
把该行修改前的值Copy到undo log,即上图中T1行
修改当前行的值,填写事务编号,使回滚指针指向undo log中的修改前的行

  上面例子里undo log中有两行记录,并且通过回滚指针连在一起。因此,如果undo log一直不删除,则会通过当前记录的回滚指针回溯到该行创建时的初始内容,所幸的是在Innodb中存在purge线程,它会查询那些比现在最老的活动事务还早的undo log,并删除它们,从而保证undo log文件不至于无限增长。
  当事务正常提交时只需要更改事务状态为Commit即可,不需做其他额外的工作,而Rollback则稍微复杂点,需要根据当前回滚指针从undo log中找出事务修改前的版本并恢复。如果事务影响的行非常多,回滚则可能会变的效率不高,根据经验值没事务行数在1000~10000之间,Innodb效率还是非常高的。很显然,Innodb是一个Commit效率比Rollback高的存储引擎。

undo-log
  undo log是为回滚而用,具体内容就是copy事务前的数据库内容(行)到undo buffer,在适合的时间把undo buffer中的内容刷新到磁盘。undo buffer与redo buffer一样,也是环形缓冲,但当缓冲满的时候,undo buffer中的内容会也会被刷新到磁盘;与redo log不同的是,磁盘上不存在单独的undo log文件,所有的undo log均存放在主ibd数据文件中(表空间),即使客户端设置了每表一个数据文件也是如此。

  undo log的具体记录字段可以稍微了解下:

  1. insert into... :含主键;
  2. delete .. :含所有字段的之前的值。
  3. update .. :含需要更新字段的之前的值;
    如果是更新主键,等同于将之前行记录的删除,然后再插入,将产生两条undo log。

快照读与当前读

事务快照

  如果我们给数据库中的事务拍一张照片的话,我们会看到:在拍照的那一瞬间,有的事务已经提交,有的正在运行中,有的事务尚未开始

img

  事务快照,黑色表示事务已提交。就如上图中的快照,trx_id小于15的事务都已经提交了,大于等于31的则尚未开始;中间的15/25还在跑,而20/30已经提交。如果你现在的事务id为25,当隔离级别为【可重复读】时:你能到哪些事务修改的数据呢?答案是显然的,已经提交的则看得见(图中黑色),还没提交的自然就看不见,否则就是脏读了。

  可时间是会变化的,假设后来15进行了提交,那我们能否看得见该事务的修改记录呢(比如 a=1 修改为了 a=2)?这个也是应该看不见的,因为如果事务15提交前我们看到的是a=1,而提交后变为a=2了,这就出现了不可重复读了,这显然和【可重复读】相悖了。
  在【可重复读】隔离级别下,一旦触发快照后,这个快照会一直存在,直至事务结束。哪些事务已提交,哪些没提交,也会在这一瞬间定格。这也就保证了我们永远都在同一张照片里面“找”数据,从而保证了【可重复读】。接下来我们来看一下怎样基于事务快照来“找”数据。

注:【可重复读】隔离级别下,事务快照的触发时机主要有:

  • 开启事务后(begin/start transaction;),执行第一条常规读SQL(select)时;
  • 开启事务时,直接开启快照:start transaction with consistent snapshot.
MVCC查询基本流程

基于数据快照和多版本数据,查询的大概过程为:

  1. 触发事务快照
  2. 根据查询条件找到的数据页中的记录,获取该数据的版本号(即写入该记录的事务trx_id
  3. 基于快照,判断这个写入记录的事务(trx_id)对于快照来讲是否可见
    3.1 如果可见,则返回结果;
    3.2 如果不可见,继续找下一个版本的数据。

我们可以用一个简单的数据结构(Read View)来记录事务快照(建议结合上节的事务快照图看):

Read_View {
  // 最小的事务id,数据版本号 < min_id 表示可见
  long min_id;      

  // 最大的事务id,数据版本号 >= max_id 表示不可见
  long max_id;      

  // 中间还在跑的事务id,数据版本号在里面则表示不可见(排除本事务,自己肯定看得到自己修改的记录)
  long[] running_ids;   

  // 是否可见
  bool canSee(long data_version_trx_id) {
    return data_version_trx_id < min_id || !running_ids.contains(data_version_trx_id);
  }
}

  MySQL的InnoDB存储引擎默认事务隔离级别是RR(可重复读), 是通过 “行排他锁+MVCC” 一起实现的,不仅可以保证可重复读,还可以部分防止幻读,而非完全防止;为什么是部分防止幻读,而不是完全防止?

  场景: 在如果事务B在事务A执行中,insert了一条数据并提交,事务A再次查询,虽然读取的是undo中的旧版本数据(防止了部分幻读),但是事务A中执行update或者delete都是可以成功的。

  因为在innodb中的操作可以分为当前读(current read)和快照读(snapshot read):

  快照读:读取的是快照版本,也就是历史版本
  简单的select操作(当然不包括 select … lock in share mode, select … for update)

  当前读:读取的是最新版本
  UPDATE、DELETE、INSERT、SELECT … LOCK IN SHARE MODE、SELECT … FOR UPDATE是当前读。当前读返回的记录都会加上锁,这样保证了了其他事务不会再并发修改这条记录。

  在RR级别下,快照读是通过MVVC(多版本控制)和undo log来实现的,当前读是通过加record lock(记录锁)和gap lock(间隙锁)来实现的。innodb在快照读的情况下并没有真正的避免幻读,但是在当前读的情况下避免了不可重复读和幻读。

  通过举例来理解快照读与当前读吧:MySQL innoDB的RR隔离级别下,假设你开启了两个事务,分别是A和B,这里有个张user表,里面有四条数据。

  当你执行select *之后,在A与B事务中都会返回4条一样的数据,这是不用想的,RR隔离级别下当执行普通的select查询时,innodb默认会执行快照读,相当于就是给你目前的状态找了一张照片,以后执行select 的时候就会返回当前照片里面的数据,当其他事务提交了也对你不造成影响,和你没关系,这就实现了可重复读,那这个照片是什么时候生成的呢?
  不是开启事务的时候,是当你第一次执行select的时候,也就是说,当A开启了事务,然后没有执行任何操作,这时候B insert了一条数据然后commit,这时候A在事务中执行select,那么就能看到有B在自己在事务中添加的那条数据…,在这之后无论再有其他事务commit都没有关系,因为照片已经生成了,而且不会再生成了,以后都会参考这张照片。

一个事务快照的创建过程可以概括为:

  • 查看当前所有的未提交并活跃的事务,存储在数组中
  • 选取未提交并活跃的事务中最小的XID,记录在快照的xmin中
  • 选取所有已提交事务中最大的XID,加1后记录在xmax中

  Read View (主要是用来做可见性判断的):创建一个新事务时,copy一份当前系统中的活跃事务列表。意思是,当前不应该被本事务看到的其他事务id列表。对于Read View快照的生成时机,也非常关键,正是因为生成时机的不同,造成了RC,RR两种隔离级别的不同可见性;

  1. 在repeatable read级别,事务在begin/start transaction之后的第一条select读操作后,会创建一个快照(Read View),将当前系统中活跃的其他事务记录记录起来;

  2. 在repeatable committed级别,事务中每条select语句都会创建一个快照(Read View);

MVVC下的CRUD

**SELECT:**当隔离级别是REPEATABLE READ时select操作,InnoDB必须每行数据来保证它符合两个条件:

  1. InnoDB必须找到一个行的版本,它至少要和事务的版本一样老(也即它的版本号不大于事务的版本号)。这保证了不管是事务开始之前,或者事务创建时,或者修改了这行数据的时候,这行数据是存在的。
  2. 这行数据的删除版本必须是未定义的或者比事务版本要大。这可以保证在事务开始之前这行数据没有被删除。
    符合这两个条件的行可能会被当作查询结果而返回。

INSERT:InnoDB为这个新行记录当前的系统版本号。
DELETE:InnoDB将当前的系统版本号设置为这一行的删除ID。
UPDATE:InnoDB会写一个这行数据的新拷贝,这个拷贝的版本为当前的系统版本号。它同时也会将这个版本号写到旧行的删除版本里。

  这种额外的记录所带来的结果就是对于大多数查询来说根本就不需要获得一个锁。只是简单地以最快的速度来读取数据,确保只选择符合条件的行。这个方案的缺点在于存储引擎必须为每一行存储更多的数据,做更多的检查工作,处理更多的善后操作。

  MVCC只工作在REPEATABLE READ和READ COMMITED隔离级别下。READ UNCOMMITED不是MVCC兼容的,因为查询不能找到适合他们事务版本的行版本;它们每次都只能读到最新的版本。SERIABLABLE也不与MVCC兼容,因为读操作会锁定他们返回的每一行数据。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值