张喜硕学长之前讲过一篇MySQL RR 与 锁,在本周又看到了RR的问题,里面提到了RR是经过MVCC实现的,可是本身对此却没什么印象,翻了翻学长的博客也没讲过,就学习一下,作个记录。git
MVCC 即多版本并发控制技术,简单的理解就是一份数据保存了多份。github
用于多事务环境下,对数据读写在不加读写锁的状况下实现互不干扰,从而实现数据库的隔离性,在事务隔离级别为Read Commit 和 Repeatable read中使用到。sql
在InnoDB中,MVCC实际上是经过undo log来实现的,但使用undo log解释起来较为复杂,因此广泛的解释是:每行记录的后面保存了两个隐藏的列,DB_TRX_ID(数据行的版本号)
和DB_ROLL_PT(删除版本号)
,这两列保存的是系统版本号,每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会做为事务的版本号,用来和查询到的每行记录的版本号进行比较。下面看看进行不一样的操做时(如下内容取RR隔离级别,固然RC也是同理,只不过select的选定范围不一样),InnoDB的行为:数据库
SELECT
InnoDB会根据如下两个条件检查每行记录:segmentfault
InnoDB为新插入的每一行保存当前系统版本号做为数据行版本号。并发
InnoDB为删除的每一行保存当前系统版本号做为行删除版本号。post
InnoDB插入一条新记录,保存当前系统版本号做为数据行版本号,同时保存当前系统版本号到原来的行做为删除版本号。性能
保存这两个额外系统版本号,使大多数读操做均可以不用加锁。这样设计使得读数据操做很简单,性能很好,而且也能保证只会读取到符合标准的行,不足之处是每行记录都须要额外的存储空间,须要作更多的行检查工做,以及一些额外的维护工做。学习
光看概念确定仍是看的不太明白的,咱们用一个例子来展现一下测试
先建立一个用户表
create table user( id int primary key auto_increment, name varchar(20));
打开navicat,新建一个查询,执行如下sql
begin; # 开始一个新的事务, 事务的版本号为1 insert into user values(NULL,'zhangsan'); insert into user values(NULL,'lisi'); commit;
此时数据库中的数据应该是这样,由于新插入的每一行会保存当前系统版本号做为数据行版本号
Id | name | DB_TRX_ID(数据行版本号) | DB_ROLL_PT(删除版本号) |
---|---|---|---|
1 | zhangsan | 1 | null |
2 | lisi | 1 | null |
此时, 咱们打开一个新的查询, 把它称做Query1
begin; # 开始一个新的事务,事务版本号为2 select * from user; # 1 select * from user; # 2 commit;
此时,执行Query1中的1
咱们再打开一个查询, 把它称做Query2
begin; # 开始一个新的事务,事务版本号为3 update user set name = 'yuzhi' where id = 1; commit;
执行Query2,以后咱们在执行Query1的2
结果和Query1的1查询到的是同样的,这符合咱们的预期,由于此时数据库中的数据应该是这样
Id | name | DB_TRX_ID(数据行的版本号) | DB_ROLL_PT(删除版本号) |
---|---|---|---|
1 | zhangsan | 1 | 3 |
1 | yunzhi | 3 | null |
2 | lisi | 1 | null |
Query1只能查询数据行版本号小于等于当前事务版本号或未定义且删除版本号大于当前事务版本号的。
删除操做同理,再也不演示,咱们对Query进行commit。
上面的例子证实了MVCC可以实现可重复读,可是MVCC是否可以避免幻读呢?咱们继续看。
咱们新建一个查询,叫作Query3
begin; # 开启一个新的事务,事务版本号4 select * from user; # 1 select * from user; # 2 update user set name='yunzhi'; # 3 select * from user; commit;
Query3的1,此时数据库中的数据应该是这样(第一条记录由于事务1已关闭,因此被清除了)
Id | name | DB_TRX_ID(数据行的版本号) | DB_ROLL_PT(删除版本号) |
---|---|---|---|
1 | yunzhi | 3 | null |
2 | lisi | 1 | null |
新建一个查询Query4,
begin; # 开启一个新的事务, 事务版本号为5 insert into user values(NULL,'wangwu'); commit;
执行Query4, 此时再执行Query3的2, 查询出来的结果为
符合预期, 由于此时数据库中的数据应该是这样
Id | name | DB_TRX_ID(数据行的版本号) | DB_ROLL_PT(删除版本号) |
---|---|---|---|
1 | yunzhi | 3 | null |
2 | lisi | 1 | null |
3 | wangwu | 5 | null |
而进行查询的事务id为4
咱们接着执行Query3的3和4
三条数据全都被修改了, 而且被查出来了!!!
在查阅了一些资料后发如今RR级别中,经过MVCC机制,虽然让数据变得可重复读,但咱们读到的数据多是历史数据,不是数据库最新的数据。这种读取历史数据的方式,咱们叫它快照读 (snapshot read),而读取数据库最新版本数据的方式,叫当前读 (current read)。
当执行select操做是innodb默认会执行快照读,会记录下此次select后的结果,以后select 的时候就会返回此次快照的数据,即便其余事务提交了不会影响当前select的数据,这就实现了可重复读了。快照的生成当在第一次执行select的时候,也就是说假设当A开启了事务,而后没有执行任何操做,这时候B insert了一条数据而后commit,这时候A执行 select,那么返回的数据中就会有B添加的那条数据。以后不管再有其余事务commit都没有关系,由于快照已经生成了,后面的select都是根据快照来的。
对于会对数据修改的操做(update、insert、delete)都是采用当前读的模式。在执行这几个操做时会读取最新的记录,即便是别的事务提交的数据也能够查询到。假设要update一条记录,可是在另外一个事务中已经delete掉这条数据而且commit了,若是update就会产生冲突,因此在update的时候须要知道最新的数据。也正是由于这样因此才致使上面咱们测试的那种状况。
select的当前读须要手动的加锁:
select * from table where ? lock in share mode; select * from table where ? for update;
同时update之后会把之前的标记为删除,而增长一条数据,因此此时数据库中的数据应该是这样
Id | name | DB_TRX_ID(数据行的版本号) | DB_ROLL_PT(删除版本号) |
---|---|---|---|
1 | yunzhi | 3 | 4 |
1 | yunzhi | 4 | null |
2 | lisi | 1 | 4 |
2 | yunzhi | 4 | null |
3 | wangwu | 5 | 4 |
3 | yunzhi | 4 | null |
这也就解释了为何后续的select能把全部数据查询出来。
MySQL可重复读的隔离级别中并非彻底解决了幻读的问题,而是解决了读数据状况下的幻读问题。而对于修改的操做依旧存在幻读问题,就是说MVCC对于幻读的解决是不完全的。
有两个办法:
若是只是执行begin
语句实际上并不会开启一个事务。
对数据进行了增删改查等操做后才会开启一个事务。
本文做者: 河北工业大学梦云智开发团队 - 李宜衡