mysql-InnoDB-mvcc

mysql-InnoDB-mvcc

概述

mvcc全称为Multi-Version Concurrency Control(多版本并发控制),出现的背景是为了减少锁的使用,事务的隔离级别是读锁和写锁在相互作用的不同结果(下文会详细的介绍),mvcc的出现是为了减少锁的使用,在保证隔离性的前提下提高效率。通过快照(版本)来实现。

锁实现隔离性

事务存在意义是保证系统的所有的数据都是符合期望的,数据的状态是一致的。数据库的理论中,为了实现这个目标需要从下面的三个点开始入手:

  1. 原子性(A)
  2. 隔离性(C)
  3. 持久性(D)

这三个共同导致了一致性(C),前三个是因,后面的是果。

隔离性保证了事务的读写不会互相影响,数据库是同时处理好多事务的,如果一次只能处理一个事务,问题就变得简单,这种完全就不会有数据的问题,事务都是串行化来访问的。除此之外,处理隔离性就是加锁。

mysql中InnoDB支持下面的几种锁

  1. 共享锁和排他锁(s锁和x锁)
  2. 意向锁
  3. 记录锁
  4. 范围锁(gap锁)
  5. next-key锁
  6. 插入意向锁
  7. auto-inc锁
  8. 专门给空间索引锁

这些锁都是InnoDB中支持的,也不是本文的重点,现代数据库基本提供了下面的三种类型锁

  1. 写锁(独占锁)

    持有写锁,持有写锁的事务才可以对数据操作,数据有写锁的时候,别的事务不能写入数据,也不能添加读锁。

  2. 读锁(共享锁)

    共享锁,持有读锁的事务可以同时访问数据(数据可以持有多个读锁),数据添加读锁之后不能再添加写锁(读写互斥)。对于持有读锁的数据,如果只有一个事务,读锁是可以升级为写锁,写入数据。

  3. 范围锁

    对于某个范围直接添加排他锁,在这个范围里面的数据是不能被写入的。比如:

    select * from t_student where age > 12 and age < 30 for update
    

事务的隔离级别是不同的锁作用范围不一致导致的结果。

mysql支持下面的几种隔离级别

  1. 串行化
  2. 可重复读
  3. 读已提交
  4. 读未提交

上面的隔离性从上到下依次减弱。

串行化

串行化是最好的隔离性,抛去性能的考虑,它是最安全的,但是隔离程度和并发度是有关系的,隔离程度越高,并发度就越低。一般来说,用不到这个。

可重复读

读锁和写锁会持有到事务结束,但是不加范围锁。可重复度解决的问题是不可重复读,不能解决的问题是幻读。(需要搞清楚这俩概念,虽然听着像绕口令一样),看下面例子分析.

幻读

幻读是值,同样的查询条件,查询得到了不同的结果(不是说某一条数据变换了,而是满足条件的数据量变化了)。比如下面的情况

现在需要统计一下年龄小于20的学生的数量:

select count(*) from t_student where age < 20 -- 事务1 还没有提交
insert into t_student(age) values(12) -- 事务2   提交
select count(*) from t_student where age < 20 -- 事务1 提交

按照事件顺序,这条查询的sql语句在同一个事务中执行了两次,中间有一个事务插入了数据,按照之前对读写锁,范围锁的介绍,读写锁会持续到事务结束但是不会添加范围锁,对于这个结果来说,两次查询就多了一条数据,因为没有范围锁,上面的sql并没有改变单个数据。这个在mysql中是没有问题的(mysql的innoDB的默认隔离级别的是可重复读,但是只是在只读事务是没有问题的,要是有更新操作就有问题,下面会详细的说),这就是幻读

读已提交

写锁会持有到事务结束,读锁在查询之后就会释放掉,没有范围锁。它解决的问题是脏读,不能解决的问题是不可重复读

不可重复度

它指的是对同一条数据的两次查询出现了不同的结果,注意,这里针对的是一条数据。

比如现在想看一下 id为5的学生的信息

select * from t_student where id=5 -- 事务1 ,事务还没有提交

现在有个事务将id=5的数据做了变更

update t_student set name = '小红' where id = 5 -- 事务2 提交

事务1再次查询

select * from t_student where id=5 -- 事务1  提交

两次查询的结果是不一样的,第一次查询添加读锁,查询语句结束之后,释放了读锁,事务1还没有提交。事务2更新数据,加写锁(这时没有锁,加锁成功)更新之后事务提交,释放写锁。事务1再次查询,两次结果不一致。产生的问题的原因在于,事务1没有贯穿事务的读锁,每次查询结束之后都会释放读锁,导致写锁加锁成功。假如说隔离级别是可重复度,再事务1第一次查询之后,事务2就不会添加写锁成功,因为读锁还没有释放掉,读写锁是互斥的。

需要看一下

读未提交

不添加读锁,写锁持续到事务结束。它不能解决的问题是脏读,解决的问题是脏写

脏读

一个事务读到了另一个事务没有提交的数据。比如:

需要将 id=5的学生的名字修改,然后发现改错了,需要回滚(事务2模拟这个操作)

select * from t_student where id=5 -- 事务1,还没有提交
update t_student set name='小红' where id=5 -- 事务2,还没有提交
select * from t_student where id=5 -- 事务1,提交
rollback -- 事务2 提交

按照事件顺序,从上到下。两次查询的数据是不一致的,在事务2还没有提交的情况下,事务1居然读到了事务2的数据。这是因为读取操作不添加读锁,这反而导致了事务1可以读取到事务2没有提交的数据。 读锁和写锁是互斥的,但要是人家根本就不添加读锁呢(有点不讲武德)。这样一来,造成了脏读。

脏写

这根本就毫无隔离性,一个事务可以修改零一个事务的数据,脏读起码能保证一个事务写的时候,别的事务需要等待锁,虽然读的时候不需要获取锁。

上面的几个隔离还有一个共同的特点,他们都是一个事务在读,另一个事务在写,是否感觉怎么都是锁,就没有一种别的机制来实现这样的操作吗?mvcc来实现。

mvcc

Multiversion concurrency control-维基百科

MVCC-百度百科

InnoDB Multi-Versioning-mysql官网介绍

mvcc的基本思想是每次修改数据,会保存当前数据的快照,通过它来支持并发访问,数据回滚,不同的隔离级别。保存的快照是保存在undo的表空间或者系统的表空间,innoDB可以使用这些信息来做回滚,也可以利用这些快照来实现隔离级别。

数据结构

innoDB在每一个行数据都添加了额外的三个字段:

  1. DB_TRX_ID

    6个字节,表示这条数据最后插入或者更新的事务标识符(事务id),删除操作会被替换为更新操作,这一行的一个指定的比特的标志位被设置表示删除。

  2. DB_ROW_ID

    6个字节,这一行的行id,插入的时候单调增加,在innoDB中如果不指定主键,它会选择为空的唯一索引,如果没有的话,就会自动生成一个隐藏的id,就是它,之后它就是聚合索引中的key,索引中节点的值就是整行数据。

  3. DB_ROLL_PTR

    7个字节的回滚指针,回滚指针指向undo log中保存的版本记录,如果更新一行,ungo log会记录版本信息,并且重新构建这一行。

在mvcc中还有几个概念需要知道

  1. 当前读

    insertupdatedeleteselect * from xxx in share mode, select * from xxx for update这些都是当前读。

  2. 快照读

    正常的select操作都是快照读。

    快照读也叫做一致性读,查询的结果是某个时间点保存的快照,如果查询的数据与此同时被别的事务更改,会通过保存在undo log中之前的快照信息来构建数据,通过这个操作可以减少锁的使用,提供并发量。

  3. readView

    快照的规则,保存了在建立快照的时候事务的一些信息。他有下面的几个字段

    • m_ids

      快照建立的时候活动的事务id。

    • min_trx_id

      最小的事务id。

    • max_trx_id

      下一个要生成的事务id,事务id是单调增大的。

    • creator_trx_id

      创建readView的事务id。

在快照读的时候,比如读一条数据的时候,会有下面的判断规则,通过它来决定数据是否可见。

  1. 如果当前数据的(DB_TRX_ID ) < min_trx_id。说明在readView生成的时候,当前数据的事务已经完成了,数据可见。
  2. DB_TRX_ID >= max_trx_id。说明在readView生成的时候,当前数据的事务还没有生成,数据不可见。
  3. min_trx_id <= DB_TRX_ID < max_trx_id
    • DB_TRX_ID 在 m_ids,说明readView生成的时候,事务还是活跃的,不可见。
    • DB_TRX_ID 不在 m_ids,说明readView生成的时候,事务已经提交了,可见。

这里说的DB_TRX_ID 可以等价于版本,不满足条件,会顺着版本链一直往上找(版本是放在undo Log中的,版本指针是DB_ROLL_PTR),如果找不到,说明不可见。

mvcc适合一个读事务,一个写事务,如果在一个读事务中又有写操作,别的事务的数据还是会收到影响的。

mvcc在innoDB默认的隔离级别 可重复读(RR)和读已提交(RC)隔离级别下,处理Select(快照读)的默认模式。另外的两个隔离级别没有必要用到mvcc,读未提交直接修改原始数据就好了,其他事务查看数据立即可以看到,不需要版本字段,可串行化本来就是为了堵塞其他事务,让操作变为线性,mvcc作为无锁优化的方式也不完全不用到。

看下面例子分析。

列子分析

表结构

CREATE TABLE `t_student` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT 'id',
  `name` varchar(12) NOT NULL DEFAULT '' COMMENT 'name',
  `age` double NOT NULL DEFAULT '0' COMMENT 'age',
  PRIMARY KEY (`id`)
) 
隔离级别(RR)

在这里插入图片描述

mysql中默认的隔离级别为RR。

现在有两个事务,来模拟一下幻读。

事务1,查询年龄小于50岁。

事务2,插入年龄为20的数据一条。

在这里插入图片描述

自己模拟一点数据,跑一下试试。

结果是没有幻读的产生,按照上面readView的规则看看。

在这里插入图片描述

注意上面,事务2的开启是在事务1 之后的。要是事务2早于事务1会怎么样

在这里插入图片描述

没有幻读产生,还是按照readView来分析分析

在这里插入图片描述

事务2满足 min_trx_id <= DB_TRX_ID < max_trx_id,并且在m_ids中,不可见。

注意

mvcc对一个读,一个写的事务很适用,要是在读的事务里面做修改操作,问题就出现了。如下所示:

在这里插入图片描述

在第一次查询之后,同样的筛选条件做更新操作,会发现比第一次查询多了一条数据。

在这里插入图片描述

第一次查了6条,第二次更新了7条,多了一条。新增加的数据也被跟新了。

在innoDB,RR的隔离级别中,在一个读事务,一个写事务的场景中,是没有幻读产生的

隔离级别(RC)

还是上面的例子背景,看例子

在这里插入图片描述

在这里插入图片描述

第二次查询是可以看到事务2插入的数据的,原因在于readView创建的时机,在RC的隔离级别下面,readView每次查询都会创建一个新的。

如果想要获取最新的数据,就不能利用快照读了,需要用当前读

select * from xxx in share mode

readView的创建时机

在RR的隔离级别下,readView是在第一次查询的时候创建的,之后的查询操作都会从第一个创建的里面读取数据。(在默认的隔离级别下RR,一个事务中所有的一致性的读都会用第一次读的时候建立的快照)

在RC的隔离级别下,ReadView每次查询都会创建。(在RC的隔离级别下都会读取当前最新的快照)。

到此,mysql-innoDB-mvcc就介绍完了。


关于博客这件事,我是把它当做我的笔记,里面有很多的内容反映了我思考的过程,因为思维有限,不免有些内容有出入,如果有问题,欢迎指出。一同探讨。谢谢。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值