事务究竟是什么东西呢?想必你们学习的时候也是对事务的概念很模糊的。接下来经过一个经典例子讲解事务。mysql
银行在两个帐户之间转帐,从A
帐户转入B帐户1000元,系统先减小A帐户的1000元,而后再为B
帐号增长1000元。若是所有执行成功,数据库处于一致性;sql
若是仅执行完A
帐户金额的修改,而没有增长B
帐户的金额,则数据库就处于不一致状态,这时就须要取消前面的操做。数据库
这过程当中会有一系列的操做,好比余额查询,余额作加减法,更新余额等,这些操做必须保证是一个总体执行,要么所有成功,要么所有失败,不能让A
帐户钱扣了,可是中途某些操做失败了,致使B
帐户更新余额失败。这样用户就不乐意了,银行这不是坑我吗?数组
事务就是要保证一组数据库操做,要么所有成功,要么所有失败。markdown
在MySQL中,事务支持是在引擎层实现的。你如今知道,MySQL是一个支持多引擎的系统,但并非全部的引擎都支持事务。并发
好比MySQL原生的MyISAM
引擎就不支持事务,这也是MyISAM被InnoDB
取代的重要缘由之一。框架
接下来会以InnoDB
为例,抽丝剥茧MySQL在事务支持方面的特定实现。性能
提到事务,你确定会想到ACID
(Atomicity
、Consistency
、Isolation
、Durability
,即原子性、一致性、隔离性、持久性),接下来咱们就要讲解其中的I,也就是隔离性。学习
当数据库上存在多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,为了解决这些问题,就有了隔离级别的概念。spa
咱们知道,隔离级别越高,效率就越低,所以咱们不少状况下须要在两者之间找到一个平衡点。
SQL标准的事务隔离级别包括:
下面我逐一为你解释:
读未提交:事务中的修改,即便没有提交,对其余事务也都是可见的,事务能够读取未提交的数据,也被称为脏读
。这个级别会致使不少问题,从性能上来讲也不会比其余隔离级别好不少,但却缺少其余级别的不少好处,通常实际应用中不多用,甚至有些数据库内部根本就没有实现。
读已提交:事务从开始直到提交以前,所作的任何修改对其余事务都是不可见的,这个级别有时候也叫作不可重复读
(Nonrepeatable Read),由于同一事务中两次执行一样的查询,可能会获得不同的结果
可重复度:同个事务中屡次查询结果是一致的,解决了不可重复读的问题。此隔离级别下仍是没法解决另一个幻读
(Phantom Read)的问题,幻读是指当某个事务在读取某个范围内的记录时,另一个事务又在该范围内插入了新的记录,以前的事务再次读取该范围的记录时,会产生幻行
串行化:顾名思义是对于同一行记录,写
会加写锁
,读
会加读锁
。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
对于上面的概念中,可能 读已提交 和可重复读比较难理解,下面会用一个例子说明这种集中隔离级别。假设数据表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语句开始执行的时候建立的。这里须要注意的是,读未提交隔离级别下直接返回记录上的最新值,没有视图概念;而串行化隔离级别下直接用加锁的方式来避免并行访问。
注意一下,每种数据库的行为会有所不同,Oracle
数据库的默认隔离界别是读提交,所以,当咱们须要进行不一样数据库种类之间迁移的时候,为了保证数据库隔离级别的一致,切记将MYSQL的隔离级别设置为读提交。
配置的方式是,将启动参数transaction-isolation
的值设置成READ-COMMITTED
。你能够用show variables
来查看当前的值。
每种隔离级别都有它本身的使用场景,你要根据本身的业务状况来定。我想你可能会问那何时须要“可重复读”的场景呢?咱们来看一个数据校对逻辑的案例。
假设你在管理一个我的银行帐户表。一个表存了每月月底的余额,一个表存了帐单明细。这时候你要作数据校对,也就是判断上个月的余额和当前余额的差额,是否与本月的帐单明细一致。你必定但愿在校对过程当中,即便有用户发生了一笔新的交易,也不影响你的校对结果。
这时候使用可重复读隔离级别就很方便。事务启动时的视图能够认为是静态的,不受其余事务更新的影响。
接下来以可重复度来展开事务隔离具体是怎么实现的。
在MySQL中,实际上每条记录在更新的时候都会同时记录一条回滚操做。记录上的最新值,经过回滚操做,均可以获得前一个状态的值。
假设一个值从1被按顺序改为了二、三、4,在回滚日志里面就会有相似下面的记录。
能够看到当前值是4,从图中能够看到在查询的时候,不一样时刻启动的事务会有不一样的read-view
。如图中看到的,在视图A
、B
、C
里面,这一个记录的值分别是一、二、4,同一条记录在系统中能够存在多个版本,就是数据库的多版本并发控制(MVCC)。
对于read-view A,要获得1,就必须将当前值依次执行图中全部的回滚操做获得。同时你会发现,即便如今有另一个事务正在将4改为5,这个事务跟read-view A、B、C对应的事务是不会冲突的。
你必定会问,回滚日志总不能一直保留吧,何时删除呢?
这是确定不能一直保留的,在不须要的时候才删除。系统会判断,当没有事务再须要用到这些回滚日志时,回滚日志会被删除。
那么何时才不须要了呢?就是当系统里没有比这个回滚日志更早的read-view
的时候。
基于上面的说明,咱们来讨论一下为何建议你尽可能不要使用长事务。
长事务意味着系统里面会存在很老的事务视图。因为这些事务随时可能访问数据库里面的任何数据,因此这个事务提交以前,数据库里面它可能用到的回滚记录都必须保留,这就会致使大量占用存储空间。
在MySQL 5.5
及之前的版本,回滚日志是跟数据字典一块儿放在ibdata
文件里的,即便长事务最终提交,回滚段被清理,文件也不会变小。我见过数据只有20GB
,而回滚段有200GB
的库。最终只好为了清理回滚段,重建整个库。
除了对回滚段的影响,长事务还占用锁资源,也可能拖垮整个库,这个咱们会在后面讲锁的时候展开。
MySQL的事务启动方式有如下几种:
begin
或 start transaction
。配套的提交语句是commit
,回滚语句是rollback
。set autocommit=0
,这个命令会将这个线程的自动提交关掉。意味着若是你只执行一个select
语句,这个事务就启动了,并且并不会自动提交。这个事务持续存在直到你主动执行commit
或 rollback
语句,或者断开链接。有些客户端链接框架会默认链接成功后先执行一个set autocommit=0
的命令。这就致使接下来的查询都在事务中,若是是长链接,就致使了意外的长事务。
所以,我会建议你老是使用set autocommit=1
, 经过显式语句的方式来启动事务。
可是有的开发同窗会纠结多一次交互的问题。对于一个须要频繁使用事务的业务,第二种方式每一个事务在开始时都不须要主动执行一次 begin
,减小了语句的交互次数。若是你也有这个顾虑,我建议你使用commit work and chain
语法。
在autocommit
为1的状况下,用begin
显式启动的事务,若是执行commit
则提交事务。若是执行 commit work and chain
,则是提交事务并自动启动下一个事务,这样也省去了再次执行begin
语句的开销。同时带来的好处是从程序开发的角度明确地知道每一个语句是否处于事务中。
你能够在information_schema
库的innodb_trx
这个表中查询长事务,好比下面这个语句,用于查找持续时间超过60s
的事务。
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60
复制代码
可重复读隔离级别下,事务在启动的时候就“拍了个快照”。请注意,这个快照是基于整个库的,这时候你确定以为难以想象,若是一个库上百G的数据,那么我启动一个事务,那MYSQL岂不是要将上百G的数据拷贝出来,这个过程不是很是慢吗?可是为何咱们平时并无感受到它🈵️呢?
事实上,咱们并不须要拷贝出这100G的数据。
咱们先来看看这个快照是怎么实现的。InnoDB
里面每一个事务有一个惟一的事务ID
,叫做transaction id
。它是在事务开始的时候向InnoDB
的事务系统申请的,是按申请顺序严格递增的。
每次事务更新数据的时候,都会生成一个新的数据版本,而且把transaction id
赋值给这个数据版本的事务ID
,记为row trx_id
。同时,旧的数据版本要保留,而且在新的数据版本中,可以有信息能够直接拿到它。这也说明了,数据表中的一行记录,可能存在多个版本(row
),每一个版本有本身的row_trx_id
.
下面用一张图说明一个记录被多个事务连续更新后的状态,以下图所示:
图中用打括号表示一行数据的4个版本,当前最新版本是V4,k的值是12,它是被transaction id
为25的事务更新的,所以它的row trx_id
也是25。
你可能会问,前面的文章不是说,语句更新会生成undo log
(回滚日志)吗?那么,undo log
在哪呢?
实际上,图2中的三个虚线箭头,就是undo log;而V一、V二、V3并非物理上真实存在的,而是每次须要的时候根据当前版本和undo log计算出来的。好比,须要V2的时候,就是经过V4依次执行U三、U2算出来。
明白了多版本和row trx_id的概念后,咱们再来想一下,InnoDB是怎么定义那个“100G”的快照的。
按照可重复读的定义,一个事务启动的时候,可以看到全部已经提交的事务结果。可是以后,这个事务执行期间,其余事务的更新对它不可见。
所以,一个事务只须要在启动的时候声明说,以我启动的时刻为准,若是一个数据版本是在我启动以前生成的,就认;若是是我启动之后才生成的,我就不认,我必需要找到它的上一个版本。
固然,若是“上一个版本”也不可见,那就得继续往前找。还有,若是是这个事务本身更新的数据,它本身仍是要认的。在实现上, InnoDB为每一个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的全部事务ID。“活跃”指的就是,启动了但还没提交。
数组里面事务ID的最小值记为低水位,当前系统里面已经建立过的事务ID的最大值加1记为高水位。
这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)。而数据版本的可见性规则,就是基于数据的row trx_id和这个一致性视图的对比结果获得的。
这个视图数组把全部的row trx_id 分红了几种不一样的状况。以下图所示:
上图是数据库版本可见性规则,对于当前事务的启动瞬间来讲,一个数据版本的row trx_id,有如下几种可能:
若是落在绿色部分,表示这个版本是已提交的事务或者是当前事务本身生成的,这个数据是可见的;
若是落在灰色部分,表示这个版本是由未来启动的事务生成的,是确定不可见的;
若是落在粉色部分,那就包括两种状况
(a) 若 row trx_id在数组中,表示这个版本是由还没提交的事务生成的,不可见;
(b) 若 row trx_id不在数组中,表示这个版本是已经提交了的事务生成的,可见。
好比,对于图2中的数据来讲,若是有一个事务,它的低水位是21,那么当它访问这一行数据时,就会从V4经过U3计算出V3,因此在它看来,这一行的值是11。
你看,有了这个声明后,系统里面随后发生的更新,是否是就跟这个事务看到的内容无关了呢?由于以后的更新,生成的版本必定属于上面的2或者3(a)的状况,而对它来讲,这些新的数据版本是不存在的,因此这个事务的快照,就是“静态”的了。
因此你如今知道了,InnoDB利用了全部数据都有多个版本的这个特性,实现了“秒级建立快照”的能力。
接下来咱们用一个例子来巩固一下MVCC的知识,例子以下:
下面是一个只有两行的表的初始化语句。
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
这个命令。
还须要注意的是,咱们的例子中若是没有特别说明,都是默认autocommit=1
。
在这个例子中,事务C没有显式地使用begin/commit
,表示这个update
语句自己就是一个事务,语句完成的时候会自动提交。事务B在更新了行以后查询; 事务A在一个只读事务中查询,而且时间顺序上是在事务B的查询以后。
让咱们想一下图中的三个事务,分析一下事务A的语句返回的结果是什么?
答案:事务B查到的k的值是3,而事务A查到的k的值是1,是否是感到有点奇怪?
接下来咱们用假设分析法,进行以下的假设:
事务A开始前,系统里面只有一个活跃事务ID是99;
事务A、B、C的版本号分别是100、10一、102,且当前系统里只有这四个事务;
三个事务开始前,(1,1)这一行数据的row trx_id是90。
这样,事务A的视图数组就是[99,100]
, 事务B的视图数组是[99,100,101]
, 事务C的视图数组是[99,100,101,102]
。
为了便于咱们分析,接下来咱们经过一个图去分析,以下图所示:
这里须要说明一下,start transaction with consistent snapshot;
的意思是从这个语句开始,建立一个持续整个事务的一致性快照。因此,在读提交隔离级别下,这个用法就没意义了,等效于普通的start transaction
。
从图中能够看到,第一个有效更新是事务C,把数据从(1,1)
改为了(1,2)
。这时候,这个数据的最新版本的row trx_id是102,而90这个版本已经成为了历史版本。
第二个有效更新是事务B,把数据从(1,2)
改为了(1,3)
。这时候,这个数据的最新版本(即row trx_id)是101,而102又成为了历史版本。
你可能注意到了,在事务A查询的时候,其实事务B尚未提交,可是它生成的(1,3)这个版本已经变成当前版本了。但这个版本对事务A必须是不可见的,不然就变成脏读了。
好,如今事务A要来读数据了,它的视图数组是[99,100]
。固然了,读数据都是从当前版本读起的。因此,事务A查询语句的读数据流程是这样的:
找到(1,3)
的时候,判断出row trx_id=101,比高水位大,处于红色区域,不可见;
接着,找到上一个历史版本,一看row trx_id=102,比高水位大,处于红色区域,不可见;
再往前找,终于找到了(1,1)
,它的row trx_id=90,比低水位小,处于绿色区域,可见。
这样执行下来,虽然期间这一行数据被修改过,可是事务A不论在何时查询,看到这行数据的结果都是一致的,因此咱们称之为一致性读。
这个判断规则是我经过一些资料和高性能MYSQL中从代码逻辑直接转译过来的,可是正如你所见,用于人肉分析可见性很麻烦。
一个数据版本,对于一个事务视图来讲,除了本身的更新老是可见之外,有三种状况:
版本未提交,不可见;
版本已提交,可是是在视图建立后提交的,不可见;
版本已提交,并且是在视图建立前提交的,可见。
如今,咱们用这个规则来判断图4中的查询结果,事务A的查询语句的视图数组是在事务A启动的时候生成的,这时候:
(1,3)
还没提交,属于状况1,不可见;
(1,2)
虽然提交了,可是是在视图数组建立以后提交的,属于状况2,不可见;
(1,1)
是在视图数组建立以前提交的,可见。
你看,去掉数字对比后,只用时间前后顺序来判断,分析起来是否是轻松多了。因此,后面咱们就都用这个规则来分析。
这时候你是否是有一个这样的疑问:事务B的update
语句,若是按照一致性读,好像结果不对哦?
事务B的视图数组是先建立的,以后事务C才提交,不是应该看不见(1,2)
吗,怎么能算出(1,3)
来?
确实如此,若是事务B在更新以前查询一次数据,这个查询返回的k的值确实是1。
可是,当它要去更新数据的时候,就不能再在历史版本上更新了,不然事务C的更新就丢失了。
所以,事务B此时的set k=k+1
是在(1,2)
的基础上进行的操做,这里就用到了这样一条规则:更新数据都是先读后写的,而这个读,只能读当前的值,称为**当前读。
所以,在更新的时候,当前读拿到的数据是(1,2)
,更新后生成了新版本的数据(1,3)
,这个新版本的row trx_id是101。因此,在执行事务B查询语句的时候,一看本身的版本号是101,最新数据的版本号也是101,是本身的更新,能够直接使用,因此查询获得的k的值是3。
这里咱们提到了一个概念,叫做当前读。其实,除了update语句外,select语句若是加锁,也是当前读。
所以,若是把事务A的查询语句select * from t 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;
复制代码
假设事务C不是立刻提交的,而是变成了下面的事务C’,会怎么样呢?以下图所示:
事务C’的不一样是,更新后并无立刻提交,在它提交前,事务B的更新语句先发起了。前面说过了,虽然事务C’还没提交,可是(1,2)
这个版本也已经生成了,而且是当前的最新版本。那么,事务B的更新语句会怎么处理呢?
这时候,咱们的两阶段锁协议就要上场了。事务C’没提交,也就是说(1,2)这个版本上的写锁还没释放。而事务B是当前读,必需要读最新版本,并且必须加锁,所以就被锁住了,必须等到事务C’释放这个锁,才能继续它的当前读。
那么回到以前的隔离界别中的事务的可重复读的能力是怎么实现的?
可重复读的核心就是一致性读(consistent read);而事务更新数据的时候,只能用当前读。若是当前的记录的行锁被其余事务占用的话,就须要进入锁等待。
而读提交的逻辑和可重复读的逻辑相似,它们最主要的区别是:
在可重复读隔离级别下,只须要在事务开始的时候建立一致性视图,以后事务里的其余查询都共用这个一致性视图;
在读提交隔离级别下,每个语句执行前都会从新算出一个新的视图。
接下来再看一下,在读提交隔离级别下,事务A和事务B的查询语句查到的k,分别应该是多少呢?以下图所示:
能够看到此时事务A的查询语句的视图数组是在执行这个语句的时候建立的,时间线上(1,2)
、(1,3)
的生成时间都在建立这个视图数组的时刻以前。
可是,在这个时刻:(1,3)
还没提交,属于状况1,不可见;(1,2)
提交了,属于状况3,可见。因此,这时候事务A查询语句返回的是k=2。显然地,事务B查询结果k=3。
本文从底层分析了MySQL的事务原理,但愿对大家有所帮助,最后别忘了点赞哟!!!