转自:https://blog.csdn.net/aoxida/article/details/50689619mysql
多版本并发控制技术已经被普遍运用于各大数据库系统中,如Oracle,MS SQL Server 2005+, Postgresql, Firebird, Maria等等,开源数据库MYSQL中流行的INNODB引擎也采用了相似的并发控制技术.本文就将结合实例来解析不一样事务隔离等级下INNODB的MVCC实现原理.sql
1 MVCC概述数据库
1.1 MVCC简介session
MVCC (Multiversion Concurrency Control),即多版本并发控制技术,它使得大部分支持行锁的事务引擎,再也不单纯的使用行锁来进行数据库的并发控制,取而代之的是,把数据库的行锁与行的多个版本结合起来,只须要很小的开销,就能够实现非锁定读,从而大大提升数据库系统的并发性能.并发
1.2 实现原理mvc
MVCC能够提供基于某个时间点的快照,使得对于事务看来,老是能够提供与事务开始时刻相一致的数据,而无论这个事务执行的时间有多长.因此在不一样的事务看来,同一时刻看到的相同行的数据多是不同的,即一个行可能有多个版本.是否听起来难以想象呢?性能
原来,为了实现mvcc, innodb对每一行都加上了两个隐含的列,其中一列存储行被更新的”时间”,另一列存储行被删除的”时间”. 可是innodb存储的并非绝对的时间,而是与时间对应的数据库系统的版本号,每当一个事务开始的时候,innodb都会给这个事务分配一个递增的版本号,因此版本号也能够被认为是事务号.对于每个”查询”语句,innodb都会把这个查询语句的版本号同这个查询语句遇到的行的版本号进行对比,而后结合不一样的事务隔离等级,来决定是否返回该行.大数据
下面分别以select、delete、 insert、 update语句来讲明:优化
1) SELECTspa
对于select语句,只有同时知足了下面两个条件的行,才能被返回:
•行的被修改版本号小于或者等于该事务号
•行的被删除版本号要么没有被定义,要么大于事务的版本号:行的删除版本号若是没有被定义,说明该行没有被删除过;若是删除版本号大于当前事务的事务号,说明该行是被该事务后面启动的事务删除的,因为是repeatable read隔离等级,后开始的事务对数据的影响不该该被先开始的事务看见,因此该行应该被返回.
2) INSERT
对新插入的行,行的更新版本被修改成该事务的事务号
3) DELETE
对于删除,innodb直接把该行的被删除版本号设置为当前的事务号,至关于标记为删除,而不是实际删除
4) UPDATE
在更新行的时候,innodb会把原来的行复制一份到回滚段中,并把当前的事务号做为该行的更新版本
1.3 MVCC的优缺点
上述策略的结果就是,在读取数据的时候,innodb几乎不用得到任何锁, 每一个查询都经过版本检查,只得到本身须要的数据版本,从而大大提升了系统的并发度.
这种策略的缺点是,为了实现多版本,innodb必须对每行增长相应的字段来存储版本信息,同时须要维护每一行的版本信息,并且在检索行的时候,须要进行版本的比较,于是下降了查询的效率;innodb还必须按期清理再也不须要的行版本,及时回收空间,这也增长了一些开销
2 INNODB支持的事务隔离等级
INNODB支持并实现了ISO标准的4个事务隔离等级,即 READ-UNCOMMITTED, READ-COMMITTED, REPEATABLE-READ, SERIALIZABLE.
1) READ UNCOMMITTED (能够读未提交的): 查询能够读取到其余事务正在修改的数据,即便其余事务的修改尚未提交.这种隔离等级没法避免脏读.
2) READ COMMITTED(只能够读已经提交的):其余事务对数据库的修改,只要已经提交,其修改的结果就是可见的,与这两个事务开始的前后顺序无关.这种隔离等级避免了脏读,可是没法实现可重复读,甚至有可能产生幻读.
3) REPEATABLE READ(可重复读):比read committed更进了一步,它只能读取在它开始以前已经提交的事务对数据库的修改,在它开始之后,全部其余事务对数据库的修改对它来讲均不可见.从而实现了可重复读,可是仍有可能幻读
4) SERIALIZABLE(可串行化):这是事务隔离等级的最高级别.其实现原理就是对于全部的query,即便是查询,也会加上读锁,避免其余事务对数据的修改.因此它成功的避免了幻读.可是代价是,数据库系统的并发处理能力大大下降,因此它不会被用到生产系统中.
咱们对MVCC和标准事务隔离等级有所了解之后,再结合实例来看看其具体表现吧.
3 不一样事务隔离等级下的MVCC实现
MVCC因为其实现原理,只支持read committed和repeatable read隔离等级,下面分别举例详细说明:
每次开始以前,都先执行以下的语句:
create database if not exists mydb;
use mydb;
drop table if exists emp;
create table `emp` ( `empno` int(11) not null auto_increment, `ename` varchar(20) default null, Primary key (empno)) engine=innodb default charset=gbk;
insert into emp values(100, "yuxiangang") ;
insert into emp values(200,"2zhaoyinggang");
insert into emp values(300,"3yihongbin");
3.1 read committed隔离等级
说明:session 1和session 2表示访问同一个数据库的两个不一样的会话.行号用来表明不一样的语句执行的时间点.
行号 |
session 1 |
session 2 |
1 |
set transaction isolation level read committed; |
|
2 |
start transaction; |
|
3 |
select * from emp where empno>=100; 查询的结果为: +-------+-------+ | empno | ename | +-------+-------+ | 100 | 1yuxiangang | | 200 | 2zhaoyinggang | | 300 | 3yihongbin | +-------+-------+ |
|
4 |
|
set transaction isolation level read committed; |
5 |
|
start transaction; |
6 |
|
update emp set ename=1 where empno=100; delete from emp where empno=200; |
说明: 修改一行,而后删除一行,可是事务不提交. |
||
7 |
select * from emp where empno>=100; 查询的结果为: +-------+-------+ | empno | ename | +-------+-------+ | 100 | 1yuxiangang | | 200 | 2zhaoyinggang | | 300 | 3yihongbin | +-------+-------+ |
|
说明:会话2的事务没有提交,因此会话1看不到会话2的事务对数据库数据的修改.可是实际上修改已经发生,会话1获取的被修改或者删除的数据,都来自于回滚段.这是经过MVCC来实现的. |
||
8 |
|
commit; |
说明: 会话2提交 |
||
9 |
select * from emp where empno>=100; 查询的结果为: +-------+-------+ | empno | ename | +-------+-------+ | 100 | 1 | | 300 | 3yihongbin | +-------+-------+ |
|
说明:当事务2提交之后,因为会话1采用的是read committed隔离等级,因此会话2的提交立刻会被会话1的事务看见.对于会话1来讲,第一次执行select * from emp where empno>=100;与第二次执行该语句,两次看到的结果不同,第一次读看到了3行,第二次只看到了2行,就像发生了幻觉,称之为幻读;第一次看到100对应的ename为1yuxiangang,第二次看到的100对应的是1,两次获取的数据内容不同,称之为不可重复读. |
3.2 repeatable read隔离等级
注意:先执行开头的全部sql语句.
行号 |
session 1 |
session 2 |
session 3 |
session 4 |
1 |
set transaction isolation level repeatable read; |
|
|
|
2 |
start transaction; |
|
|
|
3 |
select * from emp where empno>=100; 查询的结果为: +-------+-------+ | empno | ename | +-------+-------+ | 100 | 1yuxiangang | | 200 | 2zhaoyinggang | | 300 | 3yihongbin | +-------+-------+ |
|
|
|
4 |
|
|
|
set @@session.autocommit=1; |
说明: 这里让会话4能够自动提交,便于观察它对前面3个会话的影响 |
||||
|
|
|
|
update emp set ename=1 where empno=100; insert into emp values(400,"4chj"); |
说明: 会话4先更新一行数据,而后插入一行数据,并自动提交 |
||||
5 |
select * from emp where empno>=100; 查询的结果为: +-------+-------+ | empno | ename | +-------+-------+ | 100 | 1yuxiangang | | 200 | 2zhaoyinggang | | 300 | 3yihongbin | +-------+-------+ |
|
|
|
说明: 会话1执行查询,两次查询获得的结果同样.它看不到会话4对数据库的修改,虽然会话4的事务已经提交.这是由于会话4的事务是在会话1的事务以后才开始.从这里也能够看出,repeatable read实现了可重复读 |
||||
6 |
|
set transaction isolation level repeatable read;start transaction; |
|
|
7 |
|
select * from emp where empno>=100; 查询的结果为: +-------+-------+ | empno | ename | +-------+-------+ | 100 | 1 | | 200 | 2zhaoyinggang | | 300 | 3yihongbin | | 400 | 4chj | +-------+-------+ |
|
|
说明: 会话2是在会话4以后开始的,因此它看到了会话4对数据库的修改.同时能够看到,相同的查询语句,不一样的事务来执行的时候,获得的结果不同.会话2与会话3执行相同的查询就获得不同的结果. |
||||
8 |
|
|
|
update emp set ename=2 where empno=200; |
9 |
|
|
set transaction isolation level repeatable read;start transaction; |
|
10 |
|
|
select * from emp where empno>=100;查询的结果为: +-------+-------+ | empno | ename | +-------+-------+ | 100 | 1 | | 200 | 2 | | 300 | 3yihongbin | | 400 | 4chj | +-------+-------+ |
|
说明: 一样,这个会话查询到的结果与会话1和会话2的结果也不同.并且会话3看到了会话4对数据库的修改. |
||||
11 |
|
|
|
update emp set ename=4 where empno=400; |
12 |
|
|
|
select * from emp where empno>=100; 查询的结果为: +-------+-------+ | empno | ename | +-------+-------+ | 100 | 1 | | 200 | 2 | | 300 | 3yihongbin | | 400 | 4 | +-------+-------+ |
说明: 事务老是能够看到自身对数据库数据的修改,尽管别的事务可能看不到这种修改 |
||||
13 |
select * from emp where empno>=100; 查询的结果为: +-------+-------+ | empno | ename | +-------+-------+ | 100 | 1yuxiangang | | 200 | 2zhaoyinggang | | 300 | 3yihongbin | +-------+-------+ |
select * from emp where empno>=100; 查询的结果为: +-------+-------+ | empno | ename | +-------+-------+ | 100 | 1 | | 200 | 2zhaoyinggang | | 300 | 3yihongbin | | 400 | 4chj | +-------+-------+ |
select * from emp where empno>=100; 查询的结果为: +-------+-------+ | empno | ename | +-------+-------+ | 100 | 1 | | 200 | 2 | | 300 | 3yihongbin | | 400 | 4chj | +-------+-------+ |
select * from emp where empno>=100; 查询的结果为: +-------+-------+ | empno | ename | +-------+-------+ | 100 | 1 | | 200 | 2 | | 300 | 3yihongbin | | 400 | 4 | +-------+-------+ |
说明: 从上面的结果能够很清晰的看到:会话1,2,3,4执行相同的语句,即便是在同一时刻,他们看到的数据均可能不同:对于empno为100的行,有 100 1yuxiangang 和 100 1两个版本;对于empno为200的行,有 200 2zhaoyinggang 和200 2两个版本…,而每一行数据均可能存在多个版本,那么这些行组合起来获得的结果集的版本就更是不可胜数,这就是数据库多版本的由来.MVCC就是经过事务发生的不一样的时间点,与数据行的版原本进行对比,从而取回与事务开始的时间点相一致的数据,来实现非阻塞的一致读. |
||||
14 |
commit; |
commit; |
commit; |
commit; |
15 |
select * from emp where empno>=100; 查询的结果为: +-------+-------+ | empno | ename | +-------+-------+ | 100 | 1 | | 200 | 2 | | 300 | 3yihongbin | | 400 | 4 | +-------+-------+ |
select * from emp where empno>=100; 查询的结果为: +-------+-------+ | empno | ename | +-------+-------+ | 100 | 1 | | 200 | 2 | | 300 | 3yihongbin | | 400 | 4 | +-------+-------+ |
select * from emp where empno>=100; 查询的结果为: +-------+-------+ | empno | ename | +-------+-------+ | 100 | 1 | | 200 | 2 | | 300 | 3yihongbin | | 400 | 4 | +-------+-------+ |
select * from emp where empno>=100; 查询的结果为: +-------+-------+ | empno | ename | +-------+-------+ | 100 | 1 | | 200 | 2 | | 300 | 3yihongbin | | 400 | 4 | +-------+-------+ |
说明: 当全部事务都提交后,他们看到的结果都是同样的 |
4 相关的Q&A
4.1 为何select count(*)在myisam表上很快,而在Innodb的表上很慢?
由于innodb采用了MVCC技术,对于相同的行,可能同时存在多个版本,innodb必须根据查询的时间来过滤掉一些行,才能得出结果,必然要执行全表扫描,而全表扫描是很是耗时的.对于myisam的表,任何行都只有一个版本,mysql甚至不须要扫描就能够直接返回精确的统计结果,咱们用explain也能够看到,对于myisam的表,执行select count(*)的时候,mysql显示” Select tables optimized away”,查询直接被优化了;而对于innodb的表,多是全表扫描,也多是”using index”,总之,速度确定会比myisam的表慢不少.
4.2 个人数据库只是频繁更新,没有插入新数据,可是为何表空间占用会愈来愈大?
若是你在数据库中执行了大事务, innodb就会把被修改数据的前映像存放到称为回滚段的公共表空间中,并且对于索引和表中的行的多个版本,若是innodb来不及purge,或者这些行由于要提供一致读而不能被purge,就会占用愈来愈多的空间,甚至有可能短期撑爆你的硬盘.因此应用程序中须要合理控制事务的大小.
4.3 能禁用MVCC吗?
禁用MVCC能够下降innodb引擎的开销,而同时innodb又能够支持外键约束,能够实现自动恢复.MVCC自己不支持read uncommitted等级,因此能够经过设置transaction_isolation = read uncommitted 来禁用MVCC.可是任何改变innodb默认隔离等级的操做,都会起到innodb_locks_unsafe_for_binlog=off相似的效果,这会致使诸如insert into t select * from t_src 之类的语句再也不给源表t_src加锁,也再也不使用innodb的间隙锁,从而产生幻读,直接致使binlog中记录的sql语句不能正确的串行化,从而主从数据库的数据再也不一致,并且基于binlog的增量备份也再也不有效.因此除非不须要记录binlog,不然别这么作.固然咱们能够这样作来优化从库的性能,由于从库不须要记录binlog.
4.4 什么时候使用char类型,什么时候使用varchar类型的列?
在使用myisam引擎的状况下,定长表虽然可能占用较多的存储空间,可是它会加快检索和全表扫描的速度,此时适合选用char的列,而对于表中的变长的列,能够采用分表的方法把变长的列拆分出去,提升定长表的检索性能.而若是使用的是innodb的引擎,因为innodb的mvcc策略的实施,char数据类型相对于varchar类型几乎没有任何优点,反而varchar列可能节省更多的存储空间,建议使用varchar数据类型.