@[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 log
和 redo log
并不是间接写到磁盘上的,而是先写入 log buffer
。再期待适合的机会同步到OS buffer
,再由操作系统决定何时刷到磁盘,具体过程如下:
既然 undo log
和redo log
都是从 log buffer
到 OS buffer
,再到磁盘。所以中途还是有可能因为断电 / 硬件故障等起因导致日志失落。为此 MySQL 提供了三种长久化形式: 这里有一个参数innodb_flush_log_at_trx_commit
,这个参数次要管制InnoDB
将log 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 buffer
到OS buffer
),所以形式 2 比形式 0 更加高效。
理解了
undo log
和redo 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 | |
---|---|---|---|
T1 | SELECT * 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 | |
T4 | SELECT * 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,记录操作(增、删、改)该数据事务的事务 IDDB_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 | |
---|---|---|---|
T1 | SELECT * 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 | |
T4 | UPDATE student SET stu_name = ‘Katakuri’ WHERE stu_name = ‘Luffy’; | – | |
T4 | SELECT * 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 对 insert
、update
和delete
语句所应用的 以后读 (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 mode
SELECT * FROM ... for update
INSERT INTO table ...
UPDATE table SET ...
DELETE table WHERE ...
4 小结
本文次要解说了 MySQL 事务的 ACID
四大个性,undo log
和 redo log
别离实现了原子性和持久性,log 长久化的三种形式,数据库并发下的五类问题、四种隔离级别、RR 隔离级别下 select 幻读通过 MVCC 机制解决、select ... lock in share mode
/select ... for update
/insert
/update
/delete
的幻读通过 间隙锁 来解决。
本文波及的比拟深刻,把握好本文的知识点,让你不仅仅是停留在 ACID、隔离级别的层面,在面试中可能化被动为被动,收割大厂 offer。