@[TOC]

1 引言

想必加入过后盾开发面试的搭档们都晓得,MySQL事务这玩意是各大面试官百问不厌的知识点,然而大家对于事务的理解到什么层面呢,仅仅停留在ACID上么,这篇文章将陪着大家一起深刻MySQL中的事务。

2 事务的个性

引言中所提到的ACID正是事务的四个个性:别离是原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)

  • 原子性(Atomicity):事务作为一个整体被执行,蕴含在其中的对数据库的操作要么全副被执行,要么都不执行。
  • 一致性(Consistency):事务应确保数据库的状态从一个统一状态转变为另一个统一状态。统一状态的含意是数据库中的数据应满足完整性束缚。
  • 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其余事务的执行。
  • 持久性(Durability):已被提交的事务对数据库的批改应该永恒保留在数据库中。

其中一致性不太好了解,一致性是说无论事务提交还是回滚,不会毁坏数据的完整性。比方A给B转100元,如果胜利了,A的账户必然会扣100元,而B的账户必定会减少100元;如果失败了,A和B的账户余额不会扭转。A和B中的账户金额变动必然是一个残缺的过程(不可能是A扣除了50,B减少了50这种状况),整个过程必须是统一的。

2.1 原子性

事务的原子性是指:一个事务中的多个操作都是不可分割的,只能是全副执行胜利、或者全副执行失败。
MySQL事务的原子性是通过undo log来实现的。undo log是InnoDB存储引擎特有的。具体的实现机制是:将所有对数据的批改(增、删、改)都写入日志(undo log)。

<font color="#E96900">undo log是逻辑日志,能够了解为:记录和事务操作相同的SQL语句,事务执行insert语句,undo log就记录delete语句。它以追加写的形式记录日志,不会笼罩之前的日志。除此之外undo log还用来实现数据库多版本并发管制(Multiversion Concurrency Control,简称MVCC)。</font>
如果一个事务中的一部分操作曾经胜利,但另一部分操作,因为断电/零碎解体/其它的软硬件谬误而无奈胜利执行,则通过回溯日志,将曾经执行胜利的操作撤销,从而达到全副操作失败的目标。

2.2 持久性

事务的持久性是指:一个事务对数据的所有批改,都会永恒的保留在数据库中。
MySQL事务的持久性是通过redo log来实现的。redo log也是InnoDB存储引擎特有的。具体实现机制是:当产生数据批改(增、删、改)的时候,InnoDB引擎会先将记录写到redo log中,并更新内存,此时更新就算实现了。同时InnoDB引擎会在适合的机会将记录刷到磁盘中。
<font color="#E96900">redo log是物理日志,记录的是在某个数据页做了什么批改,而不是SQL语句的模式。它有固定大小,是循环写的形式记录日志,空间用完后会笼罩之前的日志。</font>

undo logredo log并不是间接写到磁盘上的,而是先写入log buffer。再期待适合的机会同步到OS buffer,再由操作系统决定何时刷到磁盘,具体过程如下:
既然undo logredo log都是从log bufferOS buffer,再到磁盘。所以中途还是有可能因为断电/硬件故障等起因导致日志失落。为此MySQL提供了三种长久化形式:这里有一个参数innodb_flush_log_at_trx_commit,这个参数次要管制InnoDBlog buffer中的数据写入OS buffer,并刷到磁盘的工夫点,取值别离为0,1,2,默认是1。这三个值的意思如下图所示:

首先查看MySQL默认设置的形式1,也就是每次提交后间接写入OS buffer,并且调用零碎函数fsync()把日志写到磁盘上。就保证数据一致性的角度来说,这种形式无疑是最平安的。然而咱们都晓得,平安大多数时候意味着效率偏低。每次提交都间接写入OS buffer并且写到磁盘,无疑会导致单位工夫内IO的次数过多而效率低下。除此之外,还有形式0和形式2。基本上都是每秒写入磁盘一次,所以效率都比形式1更高。然而形式0是把数据先写入log buffer再写入OS buffer再写入磁盘,而形式2是间接写入OS buffer,再写入磁盘,少了一次数据拷贝的过程(从log bufferOS buffer),所以形式2比形式0更加高效。

理解了undo logredo log的作用和实现机制之后,那么这两个日志具体是怎么让数据库从异样的状态复原到失常状态的呢?

<font color="#E96900">数据库系统解体后重启,此时数据库处于不统一的状态,必须先执行一个crash recovery的过程:首先读取redo log,把胜利提交然而还没来得及写入磁盘的数据从新写入磁盘,保障了持久性。再读取undo log将还没有胜利提交的事务进行回滚,保障了原子性。crash recovery完结后,数据库复原到一致性状态,能够持续被应用。</font>

2.3 隔离性

数据库事务的隔离性是指:多个事务并发执行时,一个事务的执行不应影响其余事务的执行。失常状况下,必定是多个事务同时操作同一个数据库,所以事务之间的隔离就显得必不可少。
如果没有隔离性,将会产生以下问题:

2.3.1 第一类失落更新

一个事务在撤销的时候,笼罩了另一个事务已提交的更新数据。
假如当初有两个事务A、B同时操作同一账户的金额,如下图所示:

显然,事务B在撤销事务的时候,笼罩了事务A在T4阶段曾经提交的更新数据。A在T3的时候曾经取走了200元,此时的余额应该是800元,然而因为事务B开始的时候,余额是1000元,所以回滚后,余额也会变成1000元。这样一来,用户明明取了钱,然而余额不变,银行亏到姥姥家了。

2.3.2 脏读

一个事务读到了另一个事务未提交的更新数据。
用下图阐明:

事务A在T3的时候取走了200元,然而未提交。事务B在T4时查问余额就能看到事务A未提交的更新。

2.3.3 幻读

幻读(虚读)是指:一个事务读到了另一个事务已提交的新增数据
仍然是配图阐明:

事务B在同一个事务中执行两次统计操作之间,另一事务insert了一条记录,导致失去的后果不一样,如同产生了幻觉。还有一种状况是事务B更新了表中所有记录的某一字段,之后事务A又插入了一条记录,事务B再去查问发现有一条记录没有被更新,这也是幻读。

2.3.4 不可反复读

不可反复读:一个事务读到了另一个事务已提交的更新数据
不可反复读,顾名思义,就是在同一个事务中反复读取数据会产生不统一的状况,如下图:

事务B在T2和T5阶段都执行了查问余额的操作,然而每次失去的后果都不一样,这在开发中是不容许的,同一个事务中同样的屡次查问,每次返回不一样的后果,让人未免会对数据库的可靠性产生狐疑。

2.3.5 第二类失落更新

一个事务在提交的时候,笼罩了另一个事务已提交的更新数据
由上图能够看出,当事务A提交之后,账户余额曾经产生了变动,而后事务B还是基于原始金额(即1000)的根底上扣除取款金额的,事务B以提交,就是把事务A的提交给齐全笼罩了。此为第二类失落更新。

留神和第一类失落更新辨别,第一类失落更新重点在事务B最终撤销了事务,第二类是最终提交了事务。

为了解决这五类问题,MySQL提供了四种隔离级别:

  • Serializable(串行化):事务之间以一种串行的形式执行,安全性十分高,效率低
  • Repeatable Read(可反复读):是MySQL默认的隔离级别,同一个事务中雷同的查问会看到同样的数据行,安全性较高,效率较好
  • Read Commited(读已提交):一个事务能够读到另一个事务曾经提交的数据,安全性较低,效率较高
  • Read Uncommited(读未提交):一个事务能够读到另一个事务未提交的数据,安全性低,效率高
隔离级别是否呈现第一类失落更新是否呈现脏读是否呈现虚读是否呈现不可反复读是否呈现第二类失落更新
Serializable
Repeatable Read
Read Commited
Read Uncommited

3 Repeatable Read

Repeatable Read(可反复读)是MySQL默认的隔离级别,也是应用最多的隔离级别,所以独自拿进去深刻了解很有必要。Repeatable Read无奈解决幻读(虚读)问题。上面来看一个实例。
首先创立一个表并插入一条记录:

CREATE TABLE `student` (  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',  `stu_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '学生学号',  `stu_name` varchar(100) DEFAULT NULL COMMENT '学生姓名',  `created_date` datetime NOT NULL COMMENT '创立工夫',  `modified_date` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '批改工夫',  `ldelete_flag` tinyint(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除标记,0:未删除,2:已删除',  PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='学生信息表';INSERT INTO `student` VALUES (1, 230160340, 'Carson', '2016-08-20 16:37:00', '2016-08-31 16:37:05', 0);

同样的开启两个事务,如下表所示:

工夫事务A事务B
T1SELECT * FROM student-
T2-INSERT INTO student VALUES (2, 230160310, 'Kata', '2016-08-20 16:37:00', '2016-08-31 16:37:05', 0)
T3-commit
T4SELECT * FROM student-
依照上述实践,会呈现幻读景象。也就是事务A在T4时间段的查问select会看到事务B提交的新增数据。
但要让你悲观了。

执行后果如下

和预期的后果并不统一,没有呈现幻读景象。
实际上MySQL在Repeatable Read隔离级别下,用MVCC(Multiversion Concurrency Control,多版本并发管制)解决了select一般查问的幻读景象。
具体的实现形式就是事务开始时,第一条select语句查问后果集会生成一个快照(snapshot),并且这个事务完结前,同样的select语句返回的都是这个快照的后果,而不是最新的查问后果,这就是MySQL在Repeatable Read隔离级别对一般select语句应用的快照读snapshot read)。

快照读和MVCC是什么关系?

MVCC是多版本并发管制,快照就是其中的一个版本。所以能够说MVCC实现了快照读,具体的实现形式波及到MySQL的暗藏列。MySQL会给每个表主动创立三个暗藏列:

  • DB_TRX_ID:事务ID,记录操作(增、删、改)该数据事务的事务ID
  • DB_ROLL_PTR:回滚指针,记录上一个版本的数据在undo log中的地位
  • DB_ROW_ID:暗藏ID ,创立表没有适合的索引作为聚簇索引时,会用该暗藏ID创立聚簇索引

因为undo log中记录了各个版本的数据,并且通过DB_ROLL_PTR能够找到各个历史版本,并且由DB_TRX_ID决定应用哪个版本(快照)。所以相当于undo log实现了MVCC,MVCC实现了快照读。

如此看来,MySQL的Repeatable Read隔离级别利用快照读,曾经解决了幻读的问题。
然而事实并非如此,接下来再看一个例子

工夫事务A事务B
T1SELECT * FROM student-
T2-INSERT INTO student VALUES (3, 230160312, 'Luffy', '2016-08-20 16:37:00', '2016-08-31 16:37:05', 0)
T3-commit
T4UPDATE student SET stu_name = 'Katakuri' WHERE stu_name = 'Luffy';-
T4SELECT * FROM student-

事务A在T1的时候生成快照,事务B在T2的时候插入一条数据<font color="#E96900">Luffy</font>,而后提交。在T4的时候把<font color="#E96900">Luffy</font>更新成<font color="#E96900">Katakuri</font>,依据上一个例子的教训,此时事务A是看不到<font color="#E96900">Luffy</font>这条数据的,所以更新也不会胜利,并且在T5的时候查问,和T1时候一样,只有<font color="#E96900">Carson</font>和<font color="#E96900">Kata</font>两条数据。

然而,又要让你悲观了

执行后果如下

然而执行后果却不是预期的那样,事务A不仅看到了<font color="#E96900">Luffy</font>,还把它胜利的改成了<font color="#E96900">Katakuri</font>。即便事务A胜利commit之后,再次查问还是这样。

这其实是MySQL对insertupdatedelete语句所应用的以后读(current read)。因为波及到数据的批改,所以MySQL必须拿到最新的数据能力批改,所以波及到数据的批改必定不能应用快照读(snapshot read)。因为事务A读到了事务B已提交的新增数据,所以就产生了前文所说的幻读。

那么在Repeatable Read隔离级别是怎么解决幻读的呢?

是通过间隙锁(Gap Lock)来解决的。咱们都晓得InnoDB反对行锁,并且行锁是锁住索引。而间隙锁用来锁定索引记录间隙,确保索引记录的间隙不变。间隙锁是针对事务隔离级别为Repeatable Read或以上级别而设的,间隙锁和行锁一起组成了Next-Key Lock。当InnoDB扫描索引记录的时候,会首先对索引记录加上行锁,再对索引记录两边的间隙加上间隙锁(Gap Lock)。加上间隙锁之后,<font color="red">其余事务就不能在这个间隙插入记录。这样就无效的避免了幻读的产生</font>。

默认状况下,InnoDB工作在Repeatable Read的隔离级别下,并且以<font color="gree">Next-Key Lock</font>的形式对索引行进行加锁。<font color="red">当查问的索引具备唯一性(主键、惟一索引)时,Innodb存储引擎会对<font color="gree">Next-Key Lock</font>进行优化,将其降为行锁,仅仅锁住索引自身,而不是范畴(除非锁定不存在的值)。若是一般索引,则会应用<font color="gree">Next-Key Lock</font>将记录和间隙一起锁定。</font>

应用快照读的查问语句
SELECT * FROM ...
应用以后读的语句
SELECT * FROM ... lock in share modeSELECT * FROM ... for updateINSERT INTO table ...UPDATE table SET ...DELETE table WHERE ...

4 小结

本文次要解说了MySQL事务的ACID四大个性,undo logredo log别离实现了原子性和持久性,log长久化的三种形式,数据库并发下的五类问题、四种隔离级别、RR隔离级别下select幻读通过MVCC机制解决、select ... lock in share mode/select ... for update/insert/update/delete的幻读通过间隙锁来解决。
本文波及的比拟深刻,把握好本文的知识点,让你不仅仅是停留在ACID、隔离级别的层面,在面试中可能化被动为被动,收割大厂offer。

点点关注,不会迷路