mysql-InnoDB-mvcc
概述
mvcc全称为Multi-Version Concurrency Control(多版本并发控制),出现的背景是为了减少锁的使用,事务的隔离级别是读锁和写锁在相互作用的不同结果(下文会详细的介绍),mvcc的出现是为了减少锁的使用,在保证隔离性的前提下提高效率。通过快照(版本)
来实现。
锁实现隔离性
事务存在意义是保证系统的所有的数据都是符合期望的,数据的状态是一致的。数据库的理论中,为了实现这个目标需要从下面的三个点开始入手:
- 原子性(A)
- 隔离性(C)
- 持久性(D)
这三个共同导致了一致性(C),前三个是因,后面的是果。
隔离性保证了事务的读写不会互相影响,数据库是同时处理好多事务的,如果一次只能处理一个事务,问题就变得简单,这种完全就不会有数据的问题,事务都是串行化来访问的。除此之外,处理隔离性就是加锁。
mysql中InnoDB支持下面的几种锁
- 共享锁和排他锁(s锁和x锁)
- 意向锁
- 记录锁
- 范围锁(gap锁)
- next-key锁
- 插入意向锁
- auto-inc锁
- 专门给空间索引锁
这些锁都是InnoDB中支持的,也不是本文的重点,现代数据库基本提供了下面的三种类型锁
-
写锁(独占锁)
持有写锁,持有写锁的事务才可以对数据操作,数据有写锁的时候,别的事务不能写入数据,也不能添加读锁。
-
读锁(共享锁)
共享锁,持有读锁的事务可以同时访问数据(数据可以持有多个读锁),数据添加读锁之后不能再添加写锁(读写互斥)。对于持有读锁的数据,如果只有一个事务,读锁是可以升级为写锁,写入数据。
-
范围锁
对于某个范围直接添加排他锁,在这个范围里面的数据是不能被写入的。比如:
select * from t_student where age > 12 and age < 30 for update
事务的隔离级别是不同的锁作用范围不一致导致的结果。
mysql支持下面的几种隔离级别
- 串行化
- 可重复读
- 读已提交
- 读未提交
上面的隔离性从上到下依次减弱。
串行化
串行化是最好的隔离性,抛去性能的考虑,它是最安全的,但是隔离程度和并发度是有关系的,隔离程度越高,并发度就越低。一般来说,用不到这个。
可重复读
读锁和写锁会持有到事务结束,但是不加范围锁。可重复度解决的问题是不可重复读
,不能解决的问题是幻读
。(需要搞清楚这俩概念,虽然听着像绕口令一样),看下面例子分析.
幻读
幻读是值,同样的查询条件,查询得到了不同的结果(不是说某一条数据变换了,而是满足条件的数据量变化了)。比如下面的情况
现在需要统计一下年龄小于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-维基百科
InnoDB Multi-Versioning-mysql官网介绍
mvcc的基本思想是每次修改数据,会保存当前数据的快照,通过它来支持并发访问,数据回滚,不同的隔离级别。保存的快照是保存在undo的表空间或者系统的表空间,innoDB可以使用这些信息来做回滚,也可以利用这些快照来实现隔离级别。
数据结构
innoDB在每一个行数据都添加了额外的三个字段:
-
DB_TRX_ID
6个字节,表示这条数据最后插入或者更新的事务标识符(事务id),删除操作会被替换为更新操作,这一行的一个指定的比特的标志位被设置表示删除。
-
DB_ROW_ID
6个字节,这一行的行id,插入的时候单调增加,在innoDB中如果不指定主键,它会选择为空的唯一索引,如果没有的话,就会自动生成一个隐藏的id,就是它,之后它就是聚合索引中的key,索引中节点的值就是整行数据。
-
DB_ROLL_PTR
7个字节的回滚指针,回滚指针指向undo log中保存的版本记录,如果更新一行,ungo log会记录版本信息,并且重新构建这一行。
在mvcc中还有几个概念需要知道
-
当前读
insert
,update
,delete
,select * from xxx in share mode
,select * from xxx for update
这些都是当前读。 -
快照读
正常的select操作都是快照读。
快照读也叫做一致性读,查询的结果是某个时间点保存的快照,如果查询的数据与此同时被别的事务更改,会通过保存在undo log中之前的快照信息来构建数据,通过这个操作可以减少锁的使用,提供并发量。
-
readView
快照的规则,保存了在建立快照的时候事务的一些信息。他有下面的几个字段
-
m_ids
快照建立的时候活动的事务id。
-
min_trx_id
最小的事务id。
-
max_trx_id
下一个要生成的事务id,事务id是单调增大的。
-
creator_trx_id
创建readView的事务id。
-
在快照读的时候,比如读一条数据的时候,会有下面的判断规则,通过它来决定数据是否可见。
- 如果当前数据的(
DB_TRX_ID ) < min_trx_id
。说明在readView生成的时候,当前数据的事务已经完成了,数据可见。 DB_TRX_ID >= max_trx_id
。说明在readView生成的时候,当前数据的事务还没有生成,数据不可见。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就介绍完了。
关于博客这件事,我是把它当做我的笔记,里面有很多的内容反映了我思考的过程,因为思维有限,不免有些内容有出入,如果有问题,欢迎指出。一同探讨。谢谢。