事务是保证一组数据库的操做,要么所有成功,要么所有失败,这些操做必须保证是一体的,能够理解为事务是并发控制的一个基本单位,事务的的四大特性ACID是事务的基础。在MySQL中,事务的支持是在引擎层出现的。在这篇文章中,咱们将会重点讲解事务的四大特性ACID、多版本控制MVCC、当前读和一致性读。html
原子性是指一个事务是一个不可分割的单位,其中的操做要么成功,要么失败,保证了这些操做是一体的,若是操做执行失败,则已经执行的语句必须回滚退回事务以前的状态。mysql
实现原子性的关键就是当发生错误时可以回滚,InnoDB实现回滚依赖于undo log(回滚日志),当事务对数据库进行修改时,InnoDB会生成对应的undo log日志,若是在事务执行过程当中发生了错误,就须要调用rollback来实现事务的回滚,这就利用undo log中的信息将数据回滚到修改以前的状态,修改就是作相反的工做,若是是insert,修改就是delete。sql
举个例子,当事务执行update操做时,undo log中会包含被修改的主键、列、以及列在修改先后的信息,当回滚时,能够利用undo log内的信息恢复到以前的状态。数据库
事务的隔离性指的是在并发环境中,并发的事务是相互隔离的,即一个事务不能被其它的事务所干扰。不一样的事务在并发操做相同的数据时,每一个事务都有各自完整的空间,即一个事务内部的操做及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。编程
在学习事务的隔离性以前,咱们先学习下事务中并发会带来什么问题。数组
①脏读:一个事务读取了已被另外一个事务修改、但还没有提交的数据,若是这个事务对数据屡次修改,并未提交,则会致使其它并发的事务读取的数据不一致,这种状况被称为脏读。缓存
②不可重复读:事务A屡次读取同一个数据,事务B在事务A屡次读取的过程当中,对数据作了更新并提交,致使事务A屡次读取同一数据时,结果一致。安全
③幻读:幻读与不可重复读相似。它发生在一个事务(T1)读取了几行数据,接着另外一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些本来不存在的记录,就好像发生了幻觉同样,因此称为幻读。markdown
在SQL规范中,定义了4个事务的隔离级别,分别为读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable),隔离级别越低,系统开销就越低,可支持的并发就越高,可是隔离性也就越差。并发
①读未提交(RU):一个事务能够读取到另一个事务未提交的数据,该隔离级别最低,容许脏读、不可重复读和幻读。
②读提交(RC):一个事务能够读取到另外一个事务已提交的数据,该隔离级别能够防止脏读,可是会出现不可重复读和幻读。
③可重复读(RR):可重复读是指在事务处理的过程当中,屡次读取同一个数据时,其值和事务开始时刻是一致的,该隔离级别能够防止脏读、不可重复读,可是会出现脏读。
④串行化(Serializable):全部事物都被串行执行,即事务只能一个个的进行处理,不能并发执行,是事务隔离的最高级别,可是带来的影响是并发效率的降低。
总结
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
Read-Uncommitted | YES | YES | YES |
Read-Committed | NO | YES | YES |
Repeatable-Read | NO | NO | YES |
Serializable | NO | NO | NO |
NOTE:在InnoDB支持的事务中,其默认隔离级别是可重复读,由于他在读取的数据中加入了间隙锁,因此这种隔离级别的效果同串行化效果相同,能够防止幻读。
持久性指的是事务一旦提交,它对数据库的改变就应该是永久性的,接下来的其它操做不会对其有任何的影响。
持久性依赖于redo log,为了更流畅的讲解如下的内容,咱们先了解下Buffer Pool,Buffer Pool是InnoDB提供的缓存,它包含了磁盘中部分数据页的映射,当从数据库读取数据时,会首先从Buffer Pool中读取,若是Buffer Pool中没有,则从磁盘读取后放入Buffer Pool。当向数据库写入数据时,会首先写入Buffer Pool,Buffer Pool中修改的数据会按期刷新到磁盘中(这一过程称为刷脏)。它的做用就是为了提升数据的读写效率。
咱们以数据修改的流程来说解怎么实现持久性,当数据修改时,除了修改Buffer Pool()中的数据,还会在redo log中记录此次操做,当事务提交时,会调用fsync接口对redo log进行刷盘。若是MySQL宕机,重启时能够读取redo log中的数据,对数据库进行恢复。redo log采用的是WAL(Write-ahead logging),全部修改先写入日志,再更新到Buffer Pool,保证了数据不会因MySQL宕机而丢失,从而知足了持久性要求。
事务的一致性是指事务的执行不能破坏数据的完整性和一致性,事务的执行先后,数据库必须保证处于一致性的状态,即事务的执行结果必须使得数据库从一个一致性状态转变成另外一个一致性状态。所以当事务成功提交后,数据库里的数据处于一致性状态,若是在事务执行过程当中发生了错误,这些未完成的事务已经修改了一部分数据到数据库,这时数据库里的数据就是不一致的状态。
事务的一致性是实现的最终目标,其实现的前提是保证事务中的原子性、隔离型和持久性。
MVCC全称是Multi-Version Concurrency Control,即多版本并发控制,MVCC在MySQL InnoDB中的实现方式主要是为了提升数据库的并发性能,处理读/写操做的冲突。
在学习MVCC的原理以前,咱们先讲解下什么是快照读。
快照读是基于提升并发性能的考虑,快照读的实现是基于多版本并发控制,咱们能够能够认为MVCC是行锁的一个变种,但它在不少状况下,避免了加锁操做,下降了开销,既然是基于多版本,即快照读可能读到的并不必定是数据的最新版本,而有多是以前的历史版本。准确的说,MVCC多版本并发控制的是维持一个数据的多个版本,使得读写操做没有冲突。
InnoDB的默认隔离级别为可重复读,事务在启动的时候就会拍快照,并且每一个事务都有惟一的一个事务ID,它是在事务开始时向InnoDB事务系统申请,申请顺序是递增的。每行数据也都是有多个版本的。每次事务更新数据的时候,都会生成一个新的数据版本,而且把 transaction id 赋值给这个数据版本的事务 ID,记为 row trx_id。同时,旧的数据版本要保留,而且在新的数据版本中,可以有信息能够直接拿到它。即数据表中的一行记录就可能会有多个版本,每一个版本都有本身的row trx_id。
咱们以一段代码为例
建立表并插入数据
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); 复制代码
咱们对k依次进行更新
图中的蓝色矩形表示的是同一行数据的四个版本,即V1-->V2--->V3--->V4,它们分别被不一样的事务所更新,在可重复读的定义下,一个事务启动的时候,可以看到在此以前提交的结果,可是在执行过程当中,对其它事务的更新是不可见的,那么它是怎么实现的呢?
事实上,InnoDB会为每个事务构造一个数组,用于存放处于活动期间事务的ID,这里的活动期间的事务能够理解开启可是还未提交的事务。数组里的事务ID的最小值记为低水位,在当前系统里已经建立过的事务的最新值的ID+1构成高水位,这种视图数组被称为一致性视图
读数据时,能够根据row trx_id来判断,根据事务的启动瞬间分为如下三种状况
①若 row trx_id 在数组中,表示这个版本是由还没提交的事务生成的,不可见。
②若 row trx_id 不在数组中,表示这个版本是已经提交了的事务生成的,可见。
总结:InnoDB 的行数据有多个版本,每一个数据版本有本身的 row trx_id,每一个事务或者语句有本身的一致性视图。普通查询语句是一致性读,一致性读会根据 row trx_id 和一致性视图肯定数据版本的可见性。对于可重复读,查询只认可在事务启动前就已经提交完成的数据,对于读提交,查询只认可在语句启动前就已经提交完成的数据;而当前读,老是读取已经提交完成的最新版本。
场景
按照读写操做,咱们能够将数据库的并发场景分为三种
不会存在安全问题,因此不须要并发控制
有线程安全问题,可能会形成事务隔离性问题,可能遇到脏读,幻读,不可重复读
有线程安全问题,可能会存在更新丢失问题。
优势
在下面的文章中咱们就用实际案例来学习下一致性读、当前读
当前读能够理解为读取的是记录的最新版本。读取时要保证其余并发事务不能修改当前的记录,如下语句是当前读的
咱们一个案例来理解下当前读
建立表并插入数据
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); 复制代码
事务A | 事务B | 事务C |
---|---|---|
start transaction with consisent snapshot | ||
start transaction with consisent snapshot | ||
update t set k=k+1 where id=1; | ||
update t set k=k+1 where id=1;select k from t where id=1; | ||
select k from t where id=1 lock in share mode; commit | ||
commit |
当前读须要知足读取的数据为最新版本,咱们一次分析事务A、事务B、事务C读取的数据
事务A
事务A在以共享锁的形式读取数据时,事务B更新了数据,可是没有提交,因此对于事务A来讲不可见;事务C在事务A真正的读取数据以前,将k=k+1,即k=2,因此事务A读取的数据k为2
事务B
事务B在更改并查询数据以前,事务C已经读数据修改+1并提交了事务,因此事务B修改数据时会K=k+1=3,则事务B查询的数据就为3.
事务C
事务C是最早更改的数据并提交的,修改的数据k=k+1=2。
一致性读主要是针对普通的查询语句的,因此将事务A中的的查询语句换成正常的查询语句,即select k from t where id=1
事务A | 事务B | 事务C |
---|---|---|
start transaction with consisent snapshot | ||
start transaction with consisent snapshot | ||
update t set k=k+1 where id=1; | ||
update t set k=k+1 where id=1;select k from t where id=1; | ||
select k from t where id=1 ; commit | ||
commit |
此时,由于事务B,事务C是当前读,因此数据不会放生变化,依旧是事务C修改后K的值为2,事务B修改并查询获得的K为3。事务A由于从当前读换成了一致性读,咱们来具体分析下事务A。
事务A在开启事务的瞬间肯定了事务ID,假设为10,在事务A执行查询语句,事务B、事务C依次开启了事务,由于事务ID是单调递增的,咱们能够假设事务B的ID为20,事务C的ID为30,按照咱们MVCC,咱们把这些ID放入数组中
咱们能够看到,在事务A的ID数组中只有事务A,这是由于事务B和事务C都是在在事务A以后开启的,对事务A不可见,因此事务A读取的数据为1。
参考
[1]林晓斌.《MySQL实战45讲》
[2]https://www.cnblogs.com/kismetv/p/10331633.html
[3]https://www.jianshu.com/p/8845ddca3b23
欢迎关注公众号:10分钟编程,让咱们天天博学一点点
公众号回复success领取独家整理的学习资源