mysql mvcc多版本并发控制

事务隔离的实现方案有两种,LBCC和MVCC

LBCC

基于锁的并发控制,英文全称Lock-Based Concurrent Control。这种方案比较简单粗暴,就是一个事务去读取一条数据的时候,就上锁,不允许其他事务来操作(当然这个锁的实现也比较重要,如果我们只锁定当前一条数据依然无法解决幻读问题)。

概念:当前读

这个概念其实很好理解,MySQL加锁之后就是当前读。假如当前事务只是加共享锁,那么其他事务就不能有排他锁,也就是不能修改数据;而假如当前事务需要加排他锁,那么其他事务就不能持有任何锁。总而言之,能加锁成功,就确保了除了当前事务之外,其他事务不会对当前数据产生影响,所以自然而然的,当前事务读取到的数据就只能是最新的,而不会是快照数据(后文MVCC会解释快照读概念)。

锁定读

  在一个事务中,标准的SELECT语句是不会加锁,但是有两种情况例外。

  (1)给记录假设共享锁,这样一来的话,其它事务只能读不能修改,直到当前事务提交

            SELECT ... LOCK IN SHARE MODE

  (2)给索引记录加锁,这种情况下跟UPDATE的加锁情况是一样的

            SELECT ... FOR UPDATE

MVCC

MySQL的大多数事务型存储引擎实现的都不是简单的行级锁。基于提升并发性能的考虑,他们一般都是同事实现了多版本并发控制(MVCC,Mutil-Version Concurrency Control)。不仅仅是Mysql,包括Oracle,PostgreSQL等其他数据库系统也都实现了MVCC,但是各自的实现机制不尽相同,因为MVCC没有一个统一的实现标准。

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

MVCC的实现,是通过存储数据库在某个时间点的快照来实现的,也就是说,不管需要执行多长时间,每个事务看到的数据都是一致的。根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据是可能不一样的。

之前说到了不同存储引擎的MVCC实现是不同的,典型的有乐观(optimistic)并发控制和悲观(pessimistic)并发控制。

本文主要讲Mysql InnoDB 的MVCC实现,也是现在比较主流的存储引擎

快照读

  • 读取的是快照版本,也就是历史版本。
  • 快照读是针对上文的当前读而言,指的是在RR隔离级别下,在不加锁的情况下MySQL会根据回滚指针选择从undo log记录中获取快照数据,而不总是获取最新的数据,这也就是为什么另一个事务提交了数据,在当前事务中看到的依然是另一个事务提交之前的数据。
  • RR隔离级别快照并不是在BEGIN就开始产生了,而是要等到事务当中的第一次查询之后才会产生快照,之后的查询就只读取这个快照数据
  • 普通的SELECT就是快照读,而UPDATE、DELETE、INSERT、SELECT ...  LOCK IN SHARE MODE、SELECT ... FOR UPDATE是当前读。

增删查改实现

在InnoDB中,给每行增加两个隐藏字段来实现MVCC,一个用来记录数据行的创建时间,另一个用来记录行的过期时间(删除时间)。在实际操作中,存储的并不是时间,而是事务的版本号,每开启一个新事务,事务的版本号就会递增。

但是严格来说,InnoDB会给数据库中的每一行增加三个字段,它们分别是DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID(如果表设置了主键或者唯一索引,row_id则不会分配)。

  • DB_TRX_ID
    • 长度为6字节,存储了插入或更新语句的最后一个事务的事务ID。
  • DB_ROLL_PTR
    • 长度为7字节,称之为:回滚指针。每次对哪条聚簇索引记录有修改的时候,都会把老版本写入undo日志中。(注意插入操作的undo日志没有这个属性,因为它没有老版本),。
    • 正因为MySQL中undo log中会维护一个历史数据记录,所以我们应该养成定期提交事务的习惯,否则回滚段会越来越大,甚至占满了表空间。

于是乎,默认的隔离级别(REPEATABLE READ)下,增删查改变成了这样:

  • SELECT:InnoDB会根据以下两个条件检查每行记录,符合下面两个条件的记录,才会作为返回结果:
    • InnoDB只查找版本早于当前事务版的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么实在事务开始前已经存在的,要么是事务自身插入或者修改过的。
    • 只查询未删除(回滚指针为空)或者回滚指针大于当前事务id的数据。(这里不能等于是因为假如自己的事务删除了一条数据,会生成数据的回滚指针为当前事务id,所以必须排除掉自己删除的数据)
  • INSERT
    • 将当前事务的版本号保存至行的创建版本号
  • UPDATE
    • 新插入一行,并以当前事务的版本号作为新行的创建版本号,同时将原记录行的删除版本号设置为当前事务版本号
  • DELETE
    • 将当前事务的版本号保存至行的删除版本号

保存了这两个额外系统版本号,使大多数读操作都可以不用加锁。这样设计使得读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行。不足之处是每行记录都需要额外的存储空间,需要做更多的行检查工资,以及一些额外的维护工作。

注意:MVCC只在REPEATABLE READ 和 READ COMMITTED 两个隔离级别下工作。其他两个级别和MVCC不兼容

是不是很抽象,来个简单的模拟例子:

1、清空原先的test表,事务A插入两条数据,此时DB_TRX_ID(事务id)为1,DB_ROLL_PTR(回滚指针为null)

2、这时候事务B进行了一次查询,会得到上面的结果,事务2还没提交的时候又来了事务C,事务C插入了id=3的数据,此时表中的数据如下:

注意,这时候第3条数据的事务id为3,因为事务2也会产生一个事务id

3、这时候事务B再次进行查询,根据上面了解的,我们知道,这时候应该是查询不出王五的,所以实际上二次查询可能是这么查的:

4、假如这时候事务D又来了,把id=1的数据给删除了,这时候会把原数据的回滚指针记录为当前的事务id:4,所以此时数据如下:

5、回到事务B,继续查询,应该还是只有1和2两条数据,那么他可能是这么查询的:

6、假如这时候又来了事务E,对第2条数据进行了更新,这时候会生产一条事务id为5的数据,并把原数据的回滚指针也同时标记为当前的事务id:5,那么会得到如下数据:

根据上面猜测,执行下面的查询:

这时候发现,查出来的数据还是只有1和2两条。
 

有这样三种锁我们需要了解

  • Record Locks(记录锁):在索引记录上加锁。
  • Gap Locks(间隙锁):在索引记录之间加锁,或者在第一个索引记录之前加锁,或者在最后一个索引记录之后加锁。
  • Next-Key Locks:在索引记录上加锁,并且在索引记录之前的间隙加锁。它相当于是Record Locks与Gap Locks的一个结合。

在默认的隔离级别中,普通的SELECT用的是一致性读不加锁。而对于锁定读、UPDATE和DELETE,则需要加锁,至于加什么锁视情况而定。如果你对一个唯一索引使用了唯一的检索条件,那么只需锁定索引记录即可;如果你没有使用唯一索引作为检索条件,或者用到了索引范围扫描,那么将会使用间隙锁或者next-key锁以此来阻塞其它会话向这个范围内的间隙插入数据

普通的SELECT才是快照读,其它诸如UPDATE、删除都是当前读。修改的时候加锁这是必然的,同时为了防止幻读的出现还需要加间隙锁。

  • 一致性读保证了可用重复读 : 利用MVCC实现一致性非锁定读,这就有保证在同一个事务中多次读取相同的数据返回的结果是一样的,解决了不可重复读的问题
  • 间隙锁防止了幻读 : 利用Gap Locks和Next-Key可以阻止其它事务在锁定区间内插入数据,因此解决了幻读问题

综上所述,默认隔离级别的实现依赖于MVCC和锁,再具体一点是一致性读和锁。

参考:

https://dev.mysql.com/doc/refman/5.7/en/innodb-storage-engine.html
《高性能Mysql》

https://www.cnblogs.com/xibuhaohao/p/11065350.html

https://baijiahao.baidu.com/s?id=1669272579360136533&wfr=spider&for=pc

猜你喜欢

转载自blog.csdn.net/wangxuelei036/article/details/107163614