看了这篇文章,再也不用担心面试官文MySQL事务了!!!

674 阅读12分钟

深入浅出MySQL事务

事务是保证一组数据库的操作,要么全部成功,要么全部失败,这些操作必须保证是一体的,可以理解为事务是并发控制的一个基本单位,事务的的四大特性ACID是事务的基础。在MySQL中,事务的支持是在引擎层出现的。在这篇文章中,我们将会重点讲解事务的四大特性ACID、多版本控制MVCC、当前读和一致性读。

1、事务的四大特性ACID

1.1原子性

  • 概念

原子性是指一个事务是一个不可分割的单位,其中的操作要么成功,要么失败,保证了这些操作是一体的,如果操作执行失败,则已经执行的语句必须回滚退回事务之前的状态。

  • 实现原理

实现原子性的关键就是当发生错误时能够回滚,InnoDB实现回滚依赖于undo log(回滚日志),当事务对数据库进行修改时,InnoDB会生成对应的undo log日志,如果在事务执行过程中发生了错误,就需要调用rollback来实现事务的回滚,这就利用undo log中的信息将数据回滚到修改之前的状态,修改就是做相反的工作,如果是insert,修改就是delete。

举个例子,当事务执行update操作时,undo log中会包含被修改的主键、列、以及列在修改前后的信息,当回滚时,可以利用undo log内的信息恢复到之前的状态。

1.2隔离性

  • 概念

事务的隔离性指的是在并发环境中,并发的事务是相互隔离的,即一个事务不能被其它的事务所干扰。不同的事务在并发操作相同的数据时,每个事务都有各自完整的空间,即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。

在学习事务的隔离性之前,我们先学习下事务中并发会带来什么问题。

  • 事务的并发带来的问题

脏读:一个事务读取了已被另一个事务修改、但尚未提交的数据,如果这个事务对数据多次修改,并未提交,则会导致其它并发的事务读取的数据不一致,这种情况被称为脏读。

不可重复读:事务A多次读取同一个数据,事务B在事务A多次读取的过程中,对数据做了更新并提交,导致事务A多次读取同一数据时,结果一致。

幻读:幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

  • 隔离级别

在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支持的事务中,其默认隔离级别是可重复读,因为他在读取的数据中加入了间隙锁,所以这种隔离级别的效果同串行化效果相同,可以防止幻读。

1.3持久性

  • 概念

持久性指的是事务一旦提交,它对数据库的改变就应该是永久性的,接下来的其它操作不会对其有任何的影响。

  • 实现原理

持久性依赖于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宕机而丢失,从而满足了持久性要求。

1.4一致性

  • 概念

事务的一致性是指事务的执行不能破坏数据的完整性和一致性,事务的执行前后,数据库必须保证处于一致性的状态,即事务的执行结果必须使得数据库从一个一致性状态转变成另一个一致性状态。因此当事务成功提交后,数据库里的数据处于一致性状态,如果在事务执行过程中发生了错误,这些未完成的事务已经修改了一部分数据到数据库,这时数据库里的数据就是不一致的状态。

  • 实现原理

事务的一致性是实现的最终目标,其实现的前提是保证事务中的原子性、隔离型和持久性。

2、MVCC的实现

2.1基本概念

MVCC全称是Multi-Version Concurrency Control,即多版本并发控制,MVCC在MySQL InnoDB中的实现方式主要是为了提高数据库的并发性能,处理读/写操作的冲突。

在学习MVCC的原理之前,我们先讲解下什么是快照读。

2.2快照读

快照读是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,我们可以可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销,既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。准确的说,MVCC多版本并发控制的是维持一个数据的多个版本,使得读写操作没有冲突。

2.3实现原理

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来判断,根据事务的启动瞬间分为以下三种情况

  • 启动的事务ID在浅绿色部分,则表示这个版本是已提交的事务或者是当前事务自己生成,这个数据是可见的
  • 启动的是ID在深绿色部分。则表示这个版本是将来的事务生成的,是不可见的
  • 启动事务的ID落在黄色部分,则由以下两种情况

①若 row trx_id 在数组中,表示这个版本是由还没提交的事务生成的,不可见。

②若 row trx_id 不在数组中,表示这个版本是已经提交了的事务生成的,可见。

总结:InnoDB 的行数据有多个版本,每个数据版本有自己的 row trx_id,每个事务或者语句有自己的一致性视图。普通查询语句是一致性读,一致性读会根据 row trx_id 和一致性视图确定数据版本的可见性。对于可重复读,查询只承认在事务启动前就已经提交完成的数据,对于读提交,查询只承认在语句启动前就已经提交完成的数据;而当前读,总是读取已经提交完成的最新版本。

2.4使用场景及优点

场景

按照读写操作,我们可以将数据库的并发场景分为三种

  • 读-读

不会存在安全问题,所以不需要并发控制

  • 读-写

有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读

  • 写-写

有线程安全问题,可能会存在更新丢失问题。

优点

  • 在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
  • 同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题

在下面的文章中我们就用实际案例来学习下一致性读、当前读

3、当前读、一致性读

3.1当前读

当前读可以理解为读取的是记录的最新版本。读取时要保证其他并发事务不能修改当前的记录,以下语句是当前读的

  • select lock in share mode(共享锁)
  • select for update
  • update;insert ;delete

我们一个案例来理解下当前读

创建表并插入数据

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。

3.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领取独家整理的学习资源