关于php:mysql事务的实现原理

7次阅读

共计 6125 个字符,预计需要花费 16 分钟才能阅读完成。

此篇文章算是对 mysql 事务的一个总结,根本把 mysql 事务相干的知识点都涵盖到了,面试问来问去无非也就是这些,在理解这些之前咱们先对 mysql 在执行的过程中 有一个整体的意识,如下图

如上图所示,MySQL 服务器逻辑架构从上往下能够分为三层:

(1)第一层:解决客户端连贯、受权认证等。

(2)第二层:服务器层,负责查问语句的解析、优化、缓存以及内置函数的实现、存储过程等。

(3)第三层:存储引擎,负责 MySQL 中数据的存储和提取。MySQL 中服务器层不治理事务,事务是由存储引擎实现的。MySQL 反对事务的存储引擎有 InnoDB、NDB Cluster 等,其中 InnoDB 的应用最为宽泛;其余存储引擎不反对事务,如 MyIsam、Memory 等。

具体过程都在图中有所标注,大略看看有个意识就能够了。接下来咱们逐个总结

典型的 MySQL 事务是如下操作的:

start transaction;
……  #一条或多条 sql 语句
commit;

其中 start transaction 标识事务开始,commit 提交事务,将执行后果写入到数据库。如果 sql 语句执行呈现问题,会调用 rollback,回滚所有曾经执行胜利的 sql 语句。当然,也能够在事务中间接应用 rollback 语句进行回滚。

主动提交

MySQL 中默认采纳的是主动提交(autocommit)模式,如下所示:

mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set (0.00 sec)

在主动提交模式下,如果没有 start transaction 显式地开始一个事务,那么每个 sql 语句都会被当做一个事务执行提交操作。

通过如下形式,能够敞开 autocommit;须要留神的是,autocommit 参数是针对连贯的,在一个连贯中批改了参数,不会对其余连贯产生影响。

mysql> set autocommit =0;
Query OK, 0 rows affected (0.00 sec)

mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | OFF   |
+---------------+-------+
1 row in set (0.00 sec)

如果敞开了 autocommit,则所有的 sql 语句都在一个事务中,直到执行了 commit 或 rollback,该事务完结,同时开始了另外一个事务。

非凡操作

在 MySQL 中,存在一些非凡的命令,如果在事务中执行了这些命令,会马上强制执行 commit 提交事务;如 DDL 语句(create table/drop table/alter/table)、lock tables 语句等等。

不过,罕用的 select、insert、update 和 delete 命令,都不会强制提交事务。

事务的特点:ACID

原子性(Atomicity)

定义

原子性是指一个事务是一个不可分割的工作单位,其中的操作要么都做,要么都不做;如果事务中一个 sql 语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态。
简略来说,就是 $\color{red}{要么全副失败,要么全副胜利}$

实现原理
在阐明原子性原理之前,首先介绍一下 MySQL 的事务日志。MySQL 的日志有很多种,如二进制日志、谬误日志、查问日志、慢查问日志等,此外 InnoDB 存储引擎还提供了两种事务日志:redo log(重做日志)和 undo log(回滚日志)。其中 redo log 用于保障事务持久性;undo log 则是事务原子性和隔离性实现的根底。

上面说回 undo log。实现原子性的要害,是当事务回滚时可能撤销所有曾经胜利执行的 sql 语句。InnoDB 实现回滚,靠的是 undo log:当事务对数据库进行批改时,InnoDB 会生成对应的 undo log;如果事务执行失败或调用了 rollback,导致事务须要回滚,便能够利用 undo log 中的信息将数据回滚到批改之前的样子。

undo log 属于逻辑日志,它记录的是 sql 执行相干的信息。当产生回滚时,InnoDB 会依据 undo log 的内容做与之前相同的工作:对于每个 insert,回滚时会执行 delete;对于每个 delete,回滚时会执行 insert;对于每个 update,回滚时会执行一个相同的 update,把数据改回去。

以 update 操作为例:当事务执行 update 时,其生成的 undo log 中会蕴含被批改行的主键(以便晓得批改了哪些行)、批改了哪些列、这些列在批改前后的值等信息,回滚时便能够应用这些信息将数据还原到 update 之前的状态。


从上图能够理解到数据的变更都随同着回滚日志的产生:

(1) 产生了被批改前数据(zhangsan,1000) 的回滚日志

(2) 产生了被批改前数据(zhangsan,0) 的回滚日志

依据下面流程能够得出如下论断:

  1. 每条数据变更 (insert/update/delete) 操作都随同一条 undo log 的生成, 并且回滚日志必须先于数据长久化到磁盘上
  2. 所谓的回滚就是依据回滚日志做逆向操作,比方 delete 的逆向操作为 insert,insert 的逆向操作为 delete,update 的逆向为 update 等。

回滚过程如图

tips:undo log 也能够这么了解

当 delete 一条记录时,undo log 中会记录一条对应的 insert 记录 
当 insert 一条记录时,undo log 中会记录一条对应的 delete 记录
当 update 一条记录时,它记录一条对应相同的 update 记录

tips: 逻辑日志和物理日志的区别
看记日志的时候 是针对一行记录,就是逻辑日志 如果是一个数据页,就是物理日志

持久性(Durability)

定义

事务一旦提交,其所做的批改会永恒保留到数据库中,此时即便零碎解体批改的数据也不会失落。

实现原理:Redo log(WAL write ahead log)

先理解一下 MySQL 的数据存储机制,MySQL 的表数据是寄存在磁盘上的,因而想要存取的时候都要经验磁盘 IO, 然而即便是应用 SSD 磁盘 IO 也是十分耗费性能的。

为此,为了晋升性能 InnoDB 提供了缓冲池 (Buffer Pool),Buffer Pool 中蕴含了磁盘数据页的映射,能够当做缓存来应用:
读数据:会首先从缓冲池中读取,如果缓冲池中没有,则从磁盘读取再放入缓冲池;

写数据:会首先写入缓冲池,缓冲池中的数据会定期同步到磁盘中(这一过程称为刷脏);

下面这种缓冲池的措施尽管在性能方面带来了质的飞跃,然而它也带来了新的问题,当 MySQL 零碎宕机,断电的时候可能会丢数据!!!

因为咱们的数据曾经提交了,但此时是在缓冲池外头,还没来得及在磁盘长久化,所以咱们急需一种机制须要存一下已提交事务的数据,为复原数据应用。

于是 redo log 就派上用场了。上面看下 redo log 是什么时候产生的

既然 redo log 也须要存储,也波及磁盘 IO 为啥还用它?

(1)刷脏是随机 IO,因为每次批改的数据地位随机,但写 redo log 是追加操作,属于程序 IO。

(2)刷脏是以数据页(Page)为单位的,MySQL 默认页大小是 16KB,一个 Page 上一个小批改都要整页写入;而 redo log 中只蕴含真正须要写入的局部,有效 IO 大大减少。

redo log 与 binlog

咱们晓得,在 MySQL 中还存在 binlog(二进制日志)也能够记录写操作并用于数据的复原,但二者是有着基本的不同的:

(1)作用不同:redo log 是用于 crash recovery 的,保障 MySQL 宕机也不会影响持久性;binlog 是用于 point-in-time recovery 的,保障服务器能够基于工夫点复原数据,此外 binlog 还用于主从复制。

(2)档次不同:redo log 是 InnoDB 存储引擎实现的,而 binlog 是 MySQL 的服务器层 (能够参考文章后面对 MySQL 逻辑架构的介绍) 实现的,同时反对 InnoDB 和其余存储引擎。

(3)内容不同:redo log 是物理日志,内容基于磁盘的 Page;binlog 的内容是二进制的,依据 binlog_format 参数的不同,可能基于 sql 语句、基于数据自身或者二者的混合。

(4)写入机会不同:binlog 在事务提交时写入;redo log 的写入机会绝对多元:

后面曾提到:当事务提交时会调用 fsync 对 redo log 进行刷盘;这是默认状况下的策略,批改 innodb_flush_log_at_trx_commit 参数能够扭转该策略,但事务的持久性将无奈保障。
除了事务提交时,还有其余刷盘机会:如 master thread 每秒刷盘一次 redo log 等,这样的益处是不肯定要等到 commit 时刷盘,commit 速度大大放慢。

隔离性(Isolation)

定义

与原子性、持久性侧重于钻研事务自身不同,隔离性钻研的是不同事务之间的相互影响。隔离性是指,事务外部的操作与其余事务是隔离的,并发执行的各个事务之间不能相互烦扰。严格的隔离性,对应了事务隔离级别中的 Serializable (可串行化),但理论利用中出于性能方面的思考很少会应用可串行化。

实现原理

隔离性谋求的是并发情景下事务之间互不烦扰。简略起见,咱们仅思考最简略的读操作和写操作(临时不思考带锁读等非凡操作),那么隔离性的探讨,次要能够分为两个方面:

(一个事务)写操作对 (另一个事务) 写操作的影响:锁机制保障隔离性
(一个事务) 写操作对 (另一个事务) 读操作的影响:MVCC 保障隔离性

脏读、不可反复读和幻读

首先来看并发状况下,读操作可能存在的三类问题:

  • 脏读:以后事务 (A) 中能够读到其余事务 (B) 未提交的数据(脏数据),这种景象是脏读。举例如下(以账户余额表为例)

  • 不可反复读:在事务 A 中先后两次读取同一个数据,两次读取的后果不一样,这种景象称为不可反复读。脏读与不可反复读的区别在于:前者读到的是其余事务未提交的数据,后者读到的是其余事务已提交的数据。举例如下:

  • 幻读:在事务 A 中依照某个条件先后两次查询数据库,两次查问后果的条数不同,这种景象称为幻读。不可反复读与幻读的区别能够艰深的了解为:前者是数据变了,后者是数据的行数变了。举例如下

事务隔离级别

在理论利用中,读未提交在并发时会导致很多问题,而性能绝对于其余隔离级别进步却很无限,因而应用较少。可串行化强制事务串行,并发效率很低,只有当对数据一致性要求极高且能够承受没有并发时应用,因而应用也较少。因而在大多数数据库系统中,默认的隔离级别是读已提交 (如 Oracle) 或可反复读(后文简称 RR)。
能够通过如下两个命令别离查看隔离级别:

select @@tx_isolation;
+-----------------+
| @@tx_isolation  |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set (0.00 sec)

MVCC

RR 解决脏读、不可反复读、幻读等问题,应用的是 MVCC:MVCC 全称 Multi-Version Concurrency Control,即多版本的并发控制协议。上面的例子很好的体现了 MVCC 的特点:在同一时刻,不同的事务读取到的数据可能是不同的(即多版本)——在 T5 时刻,事务 A 和事务 C 能够读取到不同版本的数据。

MVCC 最大的长处是读不加锁,因而读写不抵触,并发性能好。InnoDB 实现 MVCC,多个版本的数据能够共存,次要是依附数据的暗藏列 (也能够称之为标记位) 和 undo log。其中数据的暗藏列包含了该行数据的版本号、删除工夫、指向 undo log 的指针等等;当读取数据时,MySQL 能够通过暗藏列判断是否须要回滚并找到回滚须要的 undo log,从而实现 MVCC;暗藏列的具体格局不再开展。

上面联合前文提到的几个问题别离阐明
脏读

当事务 A 在 T3 工夫节点读取 zhangsan 的余额时,会发现数据已被其余事务批改,且状态为未提交。此时事务 A 读取最新数据后,依据数据的 undo log 执行回滚操作,失去事务 B 批改前的数据,从而防止了脏读。

不可反复读

当事务 A 在 T2 节点第一次读取数据时,会记录该数据的版本号(数据的版本号是以 row 为单位记录的),假如版本号为 1;当事务 B 提交时,该行记录的版本号减少,假如版本号为 2;当事务 A 在 T5 再一次读取数据时,发现数据的版本号(2)大于第一次读取时记录的版本号(1),因而会依据 undo log 执行回滚操作,失去版本号为 1 时的数据,从而实现了可反复读。

幻读

InnoDB 实现的 RR 通过 next-key lock 机制防止了幻读景象。

next-key lock 是行锁的一种,实现相当于 record lock(记录锁) + gap lock(间隙锁);其特点是不仅会锁住记录自身(record lock 的性能),还会锁定一个范畴(gap lock 的性能)。当然,这里咱们探讨的是不加锁读:此时的 next-key lock 并不是真的加锁,只是为读取的数据减少了标记(标记内容包含数据的版本号等);精确起见权且称之为类 next-key lock 机制。还是以后面的例子来阐明:

当事务 A 在 T2 节点第一次读取 0 <id<5 数据时,标记的不只是 id= 1 的数据,而是将范畴 (0,5) 进行了标记,这样当 T5 时刻再次读取 0 <id<5 数据时,便能够发现 id= 2 的数据比之前标记的版本号更高,此时再联合 undo log 执行回滚操作,防止了幻读。

总结

概括来说,InnoDB 实现的 RR,通过锁机制、数据的暗藏列、undo log 和类 next-key lock,实现了肯定水平的隔离性,能够满足大多数场景的须要。不过须要阐明的是,RR 尽管防止了幻读问题,然而毕竟不是 Serializable,不能保障齐全的隔离,上面是一个例子,大家能够本人验证一下。

一致性

基本概念

一致性是指事务执行完结后,数据库的完整性束缚没有被毁坏,事务执行的前后都是非法的数据状态。数据库的完整性束缚包含但不限于:实体完整性(如行的主键存在且惟一)、列完整性(如字段的类型、大小、长度要符合要求)、外键束缚、用户自定义完整性(如转账前后,两个账户余额的和应该不变)。

实现

能够说,一致性是事务谋求的最终目标:后面提到的原子性、持久性和隔离性,都是为了保障数据库状态的一致性。此外,除了数据库层面的保障,一致性的实现也须要利用层面进行保障。

实现一致性的措施包含:

  • 保障原子性、持久性和隔离性,如果这些个性无奈保障,事务的一致性也无奈保障
  • 数据库自身提供保障,例如不容许向整形列插入字符串值、字符串长度不能超过列的限度等
  • 利用层面进行保障,例如如果转账操作只扣除转账者的余额,而没有减少接收者的余额,无论数据库实现的如许完满,也无奈保障状态的统一
正文完
 0