龙空技术网

Mysql 事务隔离级别和MVCC的关系

做好一个程序猿 414

前言:

此刻朋友们对“mysql数据库100g算大吗”都比较重视,大家都需要分析一些“mysql数据库100g算大吗”的相关内容。那么小编也在网上收集了一些有关“mysql数据库100g算大吗””的相关内容,希望同学们能喜欢,大家快快来学习一下吧!

1 隔离性与隔离级别

提到事务,肯定会想到ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一致性、隔离性、持久性),今天我们就来说说其中 I,也就是“隔离性”。

当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantomread)的问题,为了解决这些问题,就有了“隔离级别”的概

念。

在谈隔离级别之前,首先要知道,你隔离得越严实,效率就会越低。因此很多时候,我们要在二者之间寻找一个平衡点。SQL的事务隔离级别包括:

读未提交(read uncommitted)读未提交是指,一个事务还没提交时,它做的变更就能被别的事务看到。读提交(read committed)读提交是指,一个事务提交之后,它做的变更才会被其他事务看到。可重复读(repeatable read)可重复读是指,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。串行化(serializable )串行化,顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突

的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。

1.1 “读提交”和“可重复读”

假设数据表T中 只有一列,其中一行的值为1,下面是按照时间顺序执行两个事务的行为。

mysql> create table T(c int) engine=InnoDB; insert into T(c) values(1);

我们来看看在不同的隔离级别下,事务A会有哪些不同的返回结果,也就是图里面V1、V2、V3

的返回值分别是什么。

若隔离级别是“读未提交”, 则V1的值就是2。这时候事务B虽然还没有提交,但是结果已经被A看到了。因此,V2、V3也都是2。若隔离级别是“读提交”,则V1是1,V2的值是2。事务B的更新在提交后才能被A看到。所以,V3的值也是2。若隔离级别是“可重复读”,则V1、V2是1,V3是2。之所以V2还是1,遵循的就是这个要求:事务在执行期间看到的数据前后必须是一致的。若隔离级别是“串行化”,则在事务B执行“将1改成2”的时候,会被锁住。直到事务A提交后,事务B才可以继续执行。所以从A的角度看, V1、V2值是1,V3的值是2。

在实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。在“可重复读”隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图。在“读提交”隔离级别下,这个视图是在每个SQL语句开始执行的时候创建的。这里需要注意的是,“读未提交”隔离级别下直接返回记录上的最新值,没有视图概念;而“串行化”隔离级别下直接用加锁的方式来避免并行访问。

2 事务隔离的实现

理解了事务的隔离级别,我们再来看看事务隔离具体是怎么实现的。这里我们展开说明“可重复

读”。

在MySQL中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。

假设一个值从1被按顺序改成了2、3、4,在回滚日志里面就会有类似下面的记录。

当前值是4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的read-view。如图中看到的,在视图A、B、C里面,这一个记录的值分别是1、2、4,同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)。

对于read-viewA,要得到1,就必须将当前值依次执行图中所有的回滚操作得到。同时你会发现,即使现在有另外一个事务正在将4改成5,这个事务跟read-viewA、B、C对应的事务是不会冲突的。

回滚日志总不能一直保留吧,什么时候删除呢?答案是,在不需要的时候才删除。也就是说,系统会判断,当没有事务再需要用到这些回滚日志时,回滚日志会被删除。什么时候才不需要了呢?就是当系统里没有比这个回滚日志更早地read-view的时候。

3 事务到底隔离还是不隔离

举一个例子,下面是一个只有两行的表的初始化语句。

mysql> CREATE TABLE `t` (   `id` int(11) NOT NULL,   `k` int(11) DEFAULT NULL,   PRIMARY KEY (`id`) ) ENGINE=InnoDB;insert into t(id, k) values(1,1),(2,2);

begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作InnoDB表 的语句,事务才真正启动。如果你想要马上启动一个事务,可以使用start transaction with consistent snapshot 这个命令。

在这个例子中,事务C没有显式地使用begin/commit,表示这个update语句本身就是一个事务,语句完成的时候会自动提交。事务B在更新了行之后查询; 事务A在一个只读事务中查询,并且时间顺序上是在事务B的查询之后。

这时,如果我告诉你事务B查到的k的值是3,而事务A查到的k的值是1,你是不是感觉有点晕呢?今天这篇文章,我其实就是想和你说明白这个问题,希望借由把这个疑惑解开的过程,能够帮助你对InnoDB的事务和锁有更进一步的理解。

在MySQL里,有两个“视图”的概念:

一个是view。它是一个用查询语句定义的虚拟表,在调用的时候执行查询语句并生成结果。创建视图的语法是create view…,而它的查询方法与表一样。另一个是InnoDB在实现MVCC时用到的一致性读视图,即consistent read view,用于支持RC(Read Committed,读提交)和RR(Repeatable Read,可重复读)隔离级别的实现。 它没有物理结构,作用是事务执行期间用来定义“我能看到什么数据”。4 “快照”在MVCC里是怎么工作的?

在可重复读隔离级别下,事务在启动的时候就“拍了个快照”。注意,这个快照是基于整库的。 这时,你会说这看上去不太现实啊。如果一个库有100G,那么我启动一个事务,MySQL就要拷贝100G的数据出来,这个过程得多慢啊。可是,我平时的事务执行起来很快啊。

实际上,我们并不需要拷贝出这100G的数据。我们先来看看这个快照是怎么实现的:

InnoDB里面每个事务有一个唯一的事务ID,叫作transaction id。它是在事务开始的时候向InnoDB的事务系统申请的,是按申请顺序严格递增的。而每行数据也都是有多个版本的。每次事务更新数据的时候,都会生成一个新的数据版本,并且 把transaction id赋值给这个数据版本的事务ID,记为rowtrx_id。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。也就是说,数据表中的一行记录,其实可能有多个版本(row),每个版本有自己的rowtrx_id。

如下图所示,就是一个记录被多个事务连续更新后的状态。

图中虚线框里是同一行数据的4个版本,当前最新版本是V4,k的值是22,它是被transaction id

为25的事务更新的,因此它的rowtrx_id也是25。

语句更新会生成undo log(回滚日志),上图中的三个虚线箭头,就是undo log;而V1、V2、V3并不是物理上真实存在的,而是每次需要的时候根据当前版本和undo log计算出来的。比如,需要V2的时候,就是通过V4依 次执行U3、U2算出来。

明白了多版本和rowtrx_id的概念后,我们再来想一下,InnoDB是怎么定义那个“100G”的快照的。

按照可重复读的定义,一个事务启动的时候,能够看到所有已经提交的事务结果。但是之后,这

个事务执行期间,其他事务的更新对它不可见。因此,一个事务只需要在启动的时候声明说,“以我启动的时刻为准,如果一个数据版本是在我启动之前生成的,就认;如果是我启动以后才生成的,我就不认,我必须要找到它的上一个版本”。当然,如果“上一个版本”也不可见,那就得继续往前找。还有,如果是这个事务自己更新的数据,它自己还是要认的。

在实现上, InnoDB为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务ID。“活跃”指的就是,启动了但还没提交。 数组里面事务ID的最小值记为低水位,当前系统里面已经创建过的事务ID的最大值加1记为高水位。这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)。 而数据版本的可见性规则,就是基于数据的rowtrx_id和这个一致性视图的对比结果得到的。 这个视图数组把所有的rowtrx_id 分成了几种不同的情况。

这样,对于当前事务的启动瞬间来说,一个数据版本的rowtrx_id,有以下几种可能:

如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的;如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的;如果落在黄色部分,那就包括两种情况若 rowtrx_id在数组中,表示这个版本是由还没提交的事务生成的,不可见;若 rowtrx_id不在数组中,表示这个版本是已经提交了的事务生成的,可见。

我们再看这个图中的三个事务,分析下事务A的语句返回的结果,为什么是k=1。

这里,我们不妨做如下假设:

事务A开始前,系统里面只有一个活跃事务ID是99;事务A、B、C的版本号分别是100、101、102,且当前系统里只有这四个事务;三个事务开始前,(1,1)这一行数据的rowtrx_id是90。

这样,事务A的视图数组就是[99,100], 事务B的视图数组是[99,100,101], 事务C的视图数组是

[99,100,101,102]。

第一个有效更新是事务C,把数据从(1,1)改成了(1,2)。这时候,这个数据的最新版本的rowtrx_id是102,而90这个版本已经成为了历史版本。

第二个有效更新是事务B,把数据从(1,2)改成了(1,3)。这时候,这个数据的最新版本(即row

trx_id)是101,而102又成为了历史版本。

你可能注意到了,在事务A查询的时候,其实事务B还没有提交,但是它生成的(1,3)这个版本已

经变成当前版本了。但这个版本对事务A必须是不可见的,否则就变成脏读了。

对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况:

版本未提交,不可见;版本已提交,但是是在视图创建后提交的,不可见;版本已提交,而且是在视图创建前提交的,可见。

疑问:事务B的update语句,如果按照一致性读,好像结果不对?

事务B的视图数组是先生成的,之后事务C才提交,不是应该看不见(1,2)吗,怎么算出(1,3)来?

如果事务B在更新之前查询一次数据,这个查询返回的k的值确实是1。但是,当它要去更新数据的时候,就不能再在历史版本上更新了,否则事务C的更新就丢失了。因此,事务B此时的set k=k+1是在(1,2)的基础上进行的操作。

所以,这里就用到了这样一条规则:更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”。

因此,在更新的时候,当前读拿到的数据是(1,2),更新后生成了新版本的数据(1,3),这个新版本的rowtrx_id是101。所以,在执行事务B查询语句的时候,一看自己的版本号是101,最新数据的版本号也是101,是自己的更新,可以直接使用,所以查询得到的k的值是3。

这里我们提到了一个概念,叫作当前读。其实,除了update语句外,select语句如果加锁,也是

当前读。所以,如果把事务A的查询语句select *fromt where id=1修改一下,加上lock in share mode 或 for update,也都可以读到版本号是101的数据,返回的k的值是3。下面这两个select语句,就是分别加了读锁(S锁,共享锁)和写锁(X锁,排他锁)。

mysql> select k from t where id=1 lock in share mode; mysql> select k from t where id=1 for update; 
5 总结5.1 小节

MVCC,多版本的并发控制,Multi-Version Concurrency Control。

使用版本来控制并发情况下的数据问题,在B事务开始修改账户且事务未提交时,当A事务需要读取账户余额时,此时会读取到B事务修改操作之前的账户余额的副本数据,但是如果A事务需要修改账户余额数据就必须要等待B事务提交事务。

MVCC使得数据库读不会对数据加锁,普通的SELECT请求不会加锁,提高了数据库的并发处理能力。借助MVCC,数据库可以实现READ COMMITTED,REPEATABLE READ等隔离级别,用户可以查看当前数据的前一个或者前几个历史版本,保证了ACID中的I特性(隔离性)。

5.2 InnoDB的MVCC实现逻辑

MVCC可以认为是行级锁的一个变种,它可以在很多情况下避免加锁操作,因此开销更低。MVCC的实现大都都实现了非阻塞的读操作,写操作也只锁定必要的行。InnoDB的MVCC实现,是通过保存数据在某个时间点的快照来实现的。一个事务,不管其执行多长时间,其内部看到的数据是一致的。也就是事务在执行的过程中不会相互影响。下面我们简述一下MVCC在InnoDB中的实现。

  InnoDB的MVCC,通过在每行记录后面保存两个隐藏的列来实现:一个保存了行的创建时间,一个保存行的过期时间(删除时间),当然,这里的时间并不是时间戳,而是系统版本号,每开始一个新的事务,系统版本号就会递增。在RR隔离级别下,MVCC的操作如下:

select操作。InnoDB只查找版本早于(包含等于)当前事务版本的数据行。可以确保事务读取的行,要么是事务开始前就已存在,或者事务自身插入或修改的记录。行的删除版本要么未定义,要么大于当前事务版本号。可以确保事务读取的行,在事务开始之前未删除。insert操作。将新插入的行保存当前版本号为行版本号。delete操作。将删除的行保存当前版本号为删除标识。update操作。变为insert和delete操作的组合,insert的行保存当前版本号为行版本号,delete则保存当前版本号到原来的行作为删除标识。

附:MVCC 在mysql 中的实现依赖的是 undo log 与 read view 。

标签: #mysql数据库100g算大吗