MVCC(Multi-Version Concurrent Control,多版本并发控制)简介 php
MVCC(Multi-Version Concurrent Control),即多版本并发控制协议,普遍使用于数据库系统。java
在介绍MVCC概念以前,咱们先来想一下数据库系统里的一个问题:假设有多个用户同时读写数据库里的一行记录,那么怎么保证数据的一致性呢?一个基本的解决方法是对这一行记录加上一把锁,将不一样用户对同一行记录的读写操做彻底串行化执行,因为同一时刻只有一个用户在操做,所以一致性不存在问题。可是,它存在明显的性能问题:读会阻塞写,写也会阻塞读,整个数据库系统的并发性能将大打折扣。mysql
MVCC(Multi-Version Concurrent Control),即多版本并发控制协议,它的目标是在保证数据一致性的前提下,提供一种高并发的访问性能。在MVCC协议中,每一个用户在链接数据库时看到的是一个具备一致性状态的镜像,每一个事务在提交到数据库以前对其余用户均是不可见的。当事务须要更新数据时,不会直接覆盖之前的数据,而是生成一个新的版本的数据,所以一条数据会有多个版本存储,可是同一时刻只有最新的版本号是有效的。所以,读的时候就能够保证老是以当前时刻的版本的数据能够被读到,不论这条数据后来是否被修改或删除。sql
在并发读写数据库时,读操做可能会不一致的数据(脏读)。为了不这种状况,须要实现数据库的并发访问控制,最简单的方式就是加锁访问。因为,加锁会将读写操做串行化,因此不会出现不一致的状态。可是,读操做会被写操做阻塞,大幅下降读性能。在java concurrent包中,有copyonwrite系列的类,专门用于优化读远大于写的状况。而其优化的手段就是,在进行写操做时,将数据copy一份,不会影响原有数据,而后进行修改,修改完成后原子替换掉旧的数据,而读操做只会读取原有数据。经过这种方式实现写操做不会阻塞读操做,从而优化读效率。而写操做之间是要互斥的,而且每次写操做都会有一次copy,因此只适合读大于写的状况。数据库
MVCC的原理与copyonwrite相似,全称是Multi-Version Concurrent Control,即多版本并发控制。在MVCC协议下,每一个读操做会看到一个一致性的snapshot,而且能够实现非阻塞的读。MVCC容许数据具备多个版本,这个版本能够是时间戳或者是全局递增的事务ID,在同一个时间点,不一样的事务看到的数据是不一样的。session
实现原理: 并发
------------------------------------------------------------------------------------------> 时间轴分布式
|-------R(T1)-----|ide
|-----------U(T2)-----------|高并发
如上图,假设有两个并发操做R(T1)和U(T2),T1和T2是事务ID,T1小于T2,系统中包含数据a = 1(T1),R和W的操做以下:
R:read a (T1)
U:a = 2 (T2)
R(读操做)的版本T1表示要读取数据的版本,而以后写操做才会更新版本,读操做不会。在时间轴上,R晚于U,而因为U在R开始以后提交,因此对于R是不可见的。因此,R只会读取T1版本的数据,即a = 1。
因为在update操做提交以前,不能影响已有数据的一致性,因此不会改变旧的数据,update操做会被拆分红insert + delete。须要标记删除旧的数据,insert新的数据。只有update提交以后,才会影响后续的读操做。而对于读操做并且,只能读到在其以前的全部的写操做,正在执行中的写操做对其是不可见的。
上面说了一堆的虚的理论,下面来点干活,看一下MySQL的innodb引擎是如何实现MVCC的。innodb会为每一行添加两个字段,分别表示该行建立的版本和删除的版本,填入的是事务的版本号,这个版本号随着事务的建立不断递增。在repeated read的隔离级别(事务的隔离级别请看这篇文章)下,具体各类数据库操做的实现:
select:知足如下两个条件innodb会返回该行数据:(1)该行的建立版本号小于等于当前版本号,用于保证在select操做以前全部的操做已经执行落地。(2)该行的删除版本号大于当前版本或者为空。删除版本号大于当前版本意味着有一个并发事务将该行删除了。
insert:将新插入的行的建立版本号设置为当前系统的版本号。
delete:将要删除的行的删除版本号设置为当前系统的版本号。
update:不执行原地update,而是转换成insert + delete。将旧行的删除版本号设置为当前版本号,并将新行insert同时设置建立版本号为当前版本号。
其中,写操做(insert、delete和update)执行时,须要将系统版本号递增。
因为旧数据并不真正的删除,因此必须对这些数据进行清理,innodb会开启一个后台线程执行清理工做,具体的规则是将删除版本号小于当前系统版本的行删除,这个过程叫作purge。
经过MVCC很好的实现了事务的隔离性,能够达到repeated read级别,要实现serializable还必须加锁。
Mysql中的MVCC
MySQL究竟是怎么实现MVCC的?这个问题无数人都在问,但google中并没有答案,本文尝试从Mysql源码中寻找答案。
在Mysql中MVCC是在Innodb存储引擎中获得支持的,Innodb为每行记录都实现了三个隐藏字段:
6字节的事物ID用来标识该行所述的事务,7字节的回滚指针须要了解下Innodb的事务模型。
为了支持事务,Innbodb引入了下面几个概念:
下面演示下事务对某行记录的更新过程:
F1~F6是某行列的名字,1~6是其对应的数据。后面三个隐含字段分别对应该行的事务号和回滚指针,假如这条数据是刚INSERT的,能够认为ID为1,其余两个字段为空。
当事务1更改该行的值时,会进行以下操做:
与事务1相同,此时undo log,中有有两行记录,而且经过回滚指针连在一块儿。
所以,若是undo log一直不删除,则会经过当前记录的回滚指针回溯到该行建立时的初始内容,所幸的时在Innodb中存在purge线程,它会查询那些比如今最老的活动事务还早的undo log,并删除它们,从而保证undo log文件不至于无限增加。
当事务正常提交时Innbod只须要更改事务状态为COMMIT便可,不需作其余额外的工做,而Rollback则稍微复杂点,须要根据当前回滚指针从undo log中找出事务修改前的版本,并恢复。若是事务影响的行很是多,回滚则可能会变的效率不高,根据经验值没事务行数在1000~10000之间,Innodb效率仍是很是高的。很显然,Innodb是一个COMMIT效率比Rollback高的存储引擎。听说,Postgress的实现刚好与此相反。
上述过程确切地说是描述了UPDATE的事务过程,其实undo log分insert和update undo log,由于insert时,原始的数据并不存在,因此回滚时把insert undo log丢弃便可,而update undo log则必须遵照上述过程。
众所周知地是更新(update、insert、delete)是一个事务过程,在Innodb中,查询也是一个事务,只读事务。当读写事务并发访问同一行数据时,能读到什么样的内容则依赖事务级别:
读事务通常有SELECT语句触发,在Innodb中保证其非阻塞,但带FOR UPDATE的SELECT除外,带FOR UPDATE的SELECT会对行加排他锁,等待更新事务完成后读取其最新内容。就整个Innodb的设计目标来讲,就是提供高效的、非阻塞的查询操做。
上述更新前创建undo log,根据各类策略读取时非阻塞就是MVCC,undo log中的行就是MVCC中的多版本,这个可能与咱们所理解的MVCC有较大的出入,通常咱们认为MVCC有下面几个特色:
就是每行都有版本号,保存时根据版本号决定是否成功,听起来含有乐观锁的味道。。。,而Innodb的实现方式是:
两者最本质的区别是,当修改数据时是否要排他锁定,若是锁定了还算不算是MVCC?
Innodb的实现真算不上MVCC,由于并无实现核心的多版本共存,undo log中的内容只是串行化的结果,记录了多个事务的过程,不属于多版本共存。但理想的MVCC是难以实现的,当事务仅修改一行记录使用理想的MVCC模式是没有问题的,能够经过比较版本号进行回滚;但当事务影响到多行数据时,理想的MVCC据无能为力了。
好比,若是Transaciton1执行理想的MVCC,修改Row1成功,而修改Row2失败,此时须要回滚Row1,但由于Row1没有被锁定,其数据可能又被Transaction2所修改,若是此时回滚Row1的内容,则会破坏Transaction2的修改结果,致使Transaction2违反ACID。
理想MVCC难以实现的根本缘由在于企图经过乐观锁代替二段提交。修改两行数据,但为了保证其一致性,与修改两个分布式系统中的数据并没有区别,而二提交是目前这种场景保证一致性的惟一手段。二段提交的本质是锁定,乐观锁的本质是消除锁定,两者矛盾,故理想的MVCC难以真正在实际中被应用,Innodb只是借了MVCC这个名字,提供了读的非阻塞而已。
也不是说MVCC就无处可用,对一些一致性要求不高的场景和对单一数据的操做的场景仍是能够发挥做用的,好比多个事务同时更改用户在线数,若是某个事务更新失败则从新计算后重试,直至成功。这样使用MVCC会极大地提升并发数,并消除线程锁。
2017-09-25 叶师傅新班已发车 老叶茶馆
InnoDB MVCC是事务一启动就建立read view,仍是何时?
说到事务,咱们不得不先说下什么是ACID、MVCC、consistent read、read view 等几个基本概念。
ACID是事务的原子性、一致性、隔离性、持久性4个单词的首字母缩写。全部的事务型数据库系统都遵循这4个特性,InnoDB亦是如此。关于ACID的具体解释请自行 google/bing。
是multiversion concurrency control的简称,也就是多版本并发控制,是个很基本的概念。MVCC的做用是让事务在并行发生时,在必定隔离级别前提下,能够保证在某个事务中能实现一致性读,也就是该事务启动时根据某个条件读取到的数据,直到事务结束时,再次执行相同条件,仍是读到同一份数据,不会发生变化(不会看到被其余并行事务修改的数据)。
有了 MVCC 就能够提升事务的并行度,由于能够利用锁机制实现资源控制而无需等待其余事务先执行。
InnoDB MVCC使用的内部快照的意思。在不一样的隔离级别下,事务启动时(有些状况下,多是SQL语句开始时)看到的数据快照版本可能也不一样。在RR、RC、RU(READ UNCOMMITTED)等几个隔离级别下会用到 read view。
读请求基于某个时间点获得一份那时的数据快照,而无论同时其余事务对数据的修改。查询过程当中,若其余事务修改了数据,那么就须要从 undo log中获取旧版本的数据。这么作能够有效避免由于须要加锁(来阻止其余事务同时对这些数据的修改)而致使事务并行度降低的问题。
在可重复读(REPEATABLE READ,简称RR)隔离级别下,数据快照版本是在第一个读请求发起时建立的。在读已提交(READ COMMITTED,简称RC)隔离级别下,则是在每次读请求时都会从新建立一份快照。
一致性读是InnoDB在RR和RC下处理SELECT请求的默认模式。因为一致性读不会在它请求的表上加锁,其余事务能够同时修改数据不受影响。
其实,咱们从上面的解释已经明白了,在RC隔离级别下,是每一个SELECT都会获取最新的read view;而在RR隔离级别下,则是当事务中的第一个SELECT请求才建立read view。
咱们经过几个例子来增强下。
首先,确认隔离级别
yejr@imysql.com [test]>select @@tx_isolation; +-----------------+ | @@tx_isolation | +-----------------+ | REPEATABLE-READ | +-----------------+
测试1:事务启动后当即发起SELECT请求
session1 | session2 |
---|---|
begin; | begin; |
select * from t1 where a=10; +----+------+---+ | a | b | c | +----+------+---+ | 10 | 8 | 1 | |
select * from t1 where a=10; +----+------+---+ | 10 | 8 | 1 | 事务中第一个SELECT当即建立read view |
update t1 set c=10 where a=10; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 |
select * from t1 where a=10; +----+------+---+ | 10 | 8 | 1 | 再次读取,结果仍是同样 |
commit; 提交事务 |
select * from t1 where a=10; +----+------+---+ | 10 | 8 | 1 | 再次读取,结果仍然同样 |
结论可见:RR中第一个SELECT已经建立好read view,以后不会再发生变化
测试2:另外一个事物提交后才发起SELECT请求
session1 | session2 |
---|---|
begin; | begin; |
select * from t1 where a=10; +----+------+---+ | a | b | c | +----+------+---+ | 10 | 8 | 1 | |
|
update t1 set c=10 where a=10; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 |
|
commit; 提交事务 |
select * from t1 where a=10; +----+------+---+ | 10 | 8 | 10 | session1提交后才发起SELECT,能够读取到最新版本 |
结论可见:RR中是发起SELECT时才建立read view,而不是事务刚启动时就建立
根据上面提到的说法,RC隔离级别下,是每次发起SELECT都会建立read view,也就是每次SELECT都能读取到已经COMMIT的数据,因此才存在不可重复读、幻读 现象。
修改&确认隔离级别
yejr@imysql.com [test]>set session transaction isolation level read committed; yejr@imysql.com [test]>select @@tx_isolation; +-----------------+ | @@tx_isolation | +-----------------+ | REPEATABLE-READ | +-----------------+
开始测试
session1 | session2 |
---|---|
begin; | begin; |
select * from t1 where a=10; +----+------+---+ | a | b | c | +----+------+---+ | 10 | 8 | 101 | |
select * from t1 where a=10; +----+------+---+ | a | b | c | +----+------+---+ | 10 | 8 | 101 | |
update t1 set c=102 where a=10;commit; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 Query OK, 0 rows affected (0.02 sec) |
|
select * from t1 where a=10; +----+------+---+ | 10 | 8 | 102 | session1提交后再次发起SELECT,能够读取到最新版本 |
|
begin;update t1 set c=103 where a=10;commit; Query OK, 0 rows affected (0.00 sec) Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 Query OK, 0 rows affected (0.02 sec) |
|
select * from t1 where a=10; +----+------+---+ | 10 | 8 | 103 | 再次发起SELECT,又能够读取到最新版本 |
RR级别下,事务中的第一个SELECT请求才开始建立read view;
RC级别下,事务中每次SELECT请求都会从新建立read view;