一文说尽MySQL事务及ACID特性的实现原理

本文将首先介绍 MySQL 事务相关的基础概念,而后介绍事务的 ACID 特性,并分析其实现原理。MySQL 博大精深,文章疏漏之处在所不免,欢迎批评指正。sql

MySQL 事务基础概念数据库

事务(Transaction)是访问和更新数据库的程序执行单元;事务中可能包含一个或多个 sql 语句,这些语句要么都执行,要么都不执行。缓存

做为一个关系型数据库,MySQL 支持事务,本文介绍基于 MySQL 5.6。首先回顾一下 MySQL 事务的基础知识。服务器

逻辑架构和存储引擎架构

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

  • 第一层:处理客户端链接、受权认证等。
  • 第二层:服务器层,负责查询语句的解析、优化、缓存以及内置函数的实现、存储过程等。
  • 第三层:存储引擎,负责 MySQL 中数据的存储和提取。MySQL 中服务器层无论理事务,事务是由存储引擎实现的。

MySQL 支持事务的存储引擎有 InnoDB、NDB Cluster 等,其中 InnoDB 的使用最为普遍;其余存储引擎不支持事务,如 MyIsam、Memory 等。函数

如无特殊说明,后文中描述的内容都是基于 InnoDB。性能

提交和回滚学习

典型的 MySQL 事务是以下操做的:大数据

 
  1. start transaction; 
  2. ……  #一条或多条sql语句 
  3. commit; 

其中 start transaction 标识事务开始,commit 提交事务,将执行结果写入到数据库。

若是 sql 语句执行出现问题,会调用 rollback,回滚全部已经执行成功的 sql 语句。固然,也能够在事务中直接使用 rollback 语句进行回滚。

自动提交

MySQL 中默认采用的是自动提交(autocommit)模式,以下所示:

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

经过以下方式,能够关闭 autocommit;须要注意的是,autocommit 参数是针对链接的,在一个链接中修改了参数,不会对其余链接产生影响。

若是关闭了 autocommit,则全部的 sql 语句都在一个事务中,直到执行了 commit 或 rollback,该事务结束,同时开始了另一个事务。

特殊操做

在 MySQL 中,存在一些特殊的命令,若是在事务中执行了这些命令,会立刻强制执行 commit 提交事务;如 DDL 语句(create table/drop table/alter/table)、lock tables 语句等等。

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

ACID 特性

ACID 是衡量事务的四个特性:

  • 原子性(Atomicity,或称不可分割性)
  • 一致性(Consistency)
  • 隔离性(Isolation)
  • 持久性(Durability)

按照严格的标准,只有同时知足 ACID 特性才是事务;可是在各大数据库厂商的实现中,真正知足 ACID 的事务少之又少。

例如 MySQL 的 NDB Cluster 事务不知足持久性和隔离性;InnoDB 默认事务隔离级别是可重复读,不知足隔离性;Oracle 默认的事务隔离级别为 READ COMMITTED,不知足隔离性……

所以与其说 ACID 是事务必须知足的条件,不如说它们是衡量事务的四个维度。

下面将详细介绍 ACID 特性及其实现原理,为了便于理解,介绍的顺序不是严格按照 A-C-I-D。

ACID 特性及其实现原理

原子性

定义

原子性是指一个事务是一个不可分割的工做单位,其中的操做要么都作,要么都不作。

若是事务中一个 sql 语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态。

实现原理:undo log

在说明原子性原理以前,首先介绍一下 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 以前的状态。

持久性

定义

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

实现原理:redo log

redo log 和 undo log 都属于 InnoDB 的事务日志。下面先聊一下 redo log 存在的背景。

InnoDB 做为 MySQL 的存储引擎,数据是存放在磁盘中的,但若是每次读写数据都须要磁盘 IO,效率会很低。

为此,InnoDB 提供了缓存(Buffer Pool),Buffer Pool 中包含了磁盘中部分数据页的映射,做为访问数据库的缓冲:

  • 当从数据库读取数据时,会首先从 Buffer Pool 中读取,若是 Buffer Pool 中没有,则从磁盘读取后放入 Buffer Pool。
  • 当向数据库写入数据时,会首先写入 Buffer Pool,Buffer Pool 中修改的数据会按期刷新到磁盘中(这一过程称为刷脏)。

Buffer Pool 的使用大大提升了读写数据的效率,可是也带来了新的问题:若是 MySQL 宕机,而此时 Buffer Pool 中修改的数据尚未刷新到磁盘,就会致使数据的丢失,事务的持久性没法保证。

因而,redo log 被引入来解决这个问题:当数据修改时,除了修改 Buffer Pool 中的数据,还会在 redo log 记录此次操做;当事务提交时,会调用 fsync 接口对 redo log 进行刷盘。

若是 MySQL 宕机,重启时能够读取 redo log 中的数据,对数据库进行恢复。

redo log 采用的是 WAL(Write-ahead logging,预写式日志),全部修改先写入日志,再更新到 Buffer Pool,保证了数据不会因 MySQL 宕机而丢失,从而知足了持久性要求。

既然 redo log 也须要在事务提交时将日志写入磁盘,为何它比直接将 Buffer Pool 中修改的数据写入磁盘(即刷脏)要快呢?

主要有如下两方面的缘由:

  • 刷脏是随机 IO,由于每次修改的数据位置随机,但写 redo log 是追加操做,属于顺序 IO。
  • 刷脏是以数据页(Page)为单位的,MySQL 默认页大小是 16KB,一个 Page 上一个小修改都要整页写入;而 redo log 中只包含真正须要写入的部分,无效 IO 大大减小。

redo log 与 binlog

咱们知道,在 MySQL 中还存在 binlog(二进制日志)也能够记录写操做并用于数据的恢复,但两者是有着根本的不一样的。

做用不一样:

  • redo log 是用于 crash recovery 的,保证 MySQL 宕机也不会影响持久性;
  • binlog 是用于 point-in-time recovery 的,保证服务器能够基于时间点恢复数据,此外 binlog 还用于主从复制。

层次不一样:

  • redo log 是 InnoDB 存储引擎实现的,
  • 而 binlog 是 MySQL 的服务器层(能够参考文章前面对 MySQL 逻辑架构的介绍)实现的,同时支持 InnoDB 和其余存储引擎。

内容不一样:

  • redo log 是物理日志,内容基于磁盘的 Page。
  • binlog 是逻辑日志,内容是一条条 sql。

写入时机不一样:

  • redo log 的写入时机相对多元。前面曾提到,当事务提交时会调用 fsync 对 redo log 进行刷盘;这是默认状况下的策略,修改 innodb_flush_log_at_trx_commit 参数能够改变该策略,但事务的持久性将没法保证。

除了事务提交时,还有其余刷盘时机:如 master thread 每秒刷盘一次 redo log 等,这样的好处是不必定要等到 commit 时刷盘,commit 速度大大加快。

  • binlog 在事务提交时写入。

隔离性

定义

与原子性、持久性侧重于研究事务自己不一样,隔离性研究的是不一样事务之间的相互影响。

隔离性是指事务内部的操做与其余事务是隔离的,并发执行的各个事务之间不能互相干扰。

严格的隔离性,对应了事务隔离级别中的 Serializable(可串行化),但实际应用中出于性能方面的考虑不多会使用可串行化。

隔离性追求的是并发情形下事务之间互不干扰。简单起见,咱们仅考虑最简单的读操做和写操做(暂时不考虑带锁读等特殊操做)。

那么隔离性的探讨,主要能够分为两个方面:

  • (一个事务)写操做对(另外一个事务)写操做的影响:锁机制保证隔离性。
  • (一个事务)写操做对(另外一个事务)读操做的影响:MVCC 保证隔离性。

锁机制

首先来看两个事务的写操做之间的相互影响。隔离性要求同一时刻只能有一个事务对数据进行写操做,InnoDB 经过锁机制来保证这一点。

锁机制的基本原理能够归纳为:

  • 事务在修改数据以前,须要先得到相应的锁。
  • 得到锁以后,事务即可以修改数据。
  • 该事务操做期间,这部分数据是锁定的,其余事务若是须要修改数据,须要等待当前事务提交或回滚后释放锁。

行锁与表锁:按照粒度,锁能够分为表锁、行锁以及其余位于两者之间的锁。

表锁在操做数据时会锁定整张表,并发性能较差;行锁则只锁定须要操做的数据,并发性能好。

可是因为加锁自己须要消耗资源(得到锁、检查锁、释放锁等都须要消耗资源),所以在锁定数据较多状况下使用表锁能够节省大量资源。

MySQL 中不一样的存储引擎支持的锁是不同的,例如 MyIsam 只支持表锁,而 InnoDB 同时支持表锁和行锁,且出于性能考虑,绝大多数状况下使用的都是行锁。

如何查看锁信息?有多种方法能够查看 InnoDB 中锁的状况,例如:

 
  1. select * from information_schema.innodb_locks; #锁的概况 
  2. show engine innodb status; #InnoDB总体状态,其中包括锁的状况 

下面来看一个例子:

 
  1. #在事务A中执行: 
  2. start transaction; 
  3. update account SET balance = 1000 where id = 1; 
  4. 在事务B中执行: 
  5. start transaction; 
  6. update account SET balance = 2000 where id = 1; 

此时查看锁的状况:

show engine innodb status 查看锁相关的部分:

经过上述命令能够查看事务 24052 和 24053 占用锁的状况;其中 lock_type 为 RECORD,表明锁为行锁(记录锁);lock_mode 为 X,表明排它锁(写锁)。

除了排它锁(写锁)以外,MySQL 中还有共享锁(读锁)的概念。因为本文重点是 MySQL 事务的实现原理,所以对锁的介绍到此为止。

介绍完写操做之间的相互影响,下面讨论写操做对读操做的影响。

脏读、不可重复读和幻读

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

①脏读:当前事务(A)中能够读到其余事务(B)未提交的数据(脏数据),这种现象是脏读。

举例以下(以帐户余额表为例):

②不可重复读:在事务 A 中前后两次读取同一个数据,两次读取的结果不同,这种现象称为不可重复读。

脏读与不可重复读的区别在于:前者读到的是其余事务未提交的数据,后者读到的是其余事务已提交的数据。

举例以下:

③幻读:在事务 A 中按照某个条件前后两次查询数据库,两次查询结果的条数不一样,这种现象称为幻读。

不可重复读与幻读的区别能够通俗的理解为:前者是数据变了,后者是数据的行数变了。

举例以下:

事务隔离级别

sql 标准中定义了四种隔离级别,并规定了每种隔离级别下上述几个问题是否存在。

通常来讲,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差。

隔离级别与读问题的关系以下:

在实际应用中,读未提交在并发时会致使不少问题,而性能相对于其余隔离级别提升却颇有限,所以使用较少。

可串行化强制事务串行,并发效率很低,只有当对数据一致性要求极高且能够接受没有并发时使用,所以使用也较少。

所以在大多数数据库系统中,默认的隔离级别是读已提交(如 Oracle)或可重复读(后文简称 RR)。

能够经过以下两个命令分别查看全局隔离级别和本次会话的隔离级别:

InnoDB 默认的隔离级别是 RR,后文会重点介绍 RR。须要注意的是,在 SQL 标准中,RR 是没法避免幻读问题的,可是 InnoDB 实现的 RR 避免了幻读问题。

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-keylock 机制避免了幻读现象。

next-keylock 是行锁的一种,实现至关于 record lock(记录锁) + gap lock(间隙锁);其特色是不只会锁住记录自己(record lock 的功能),还会锁定一个范围(gap lock 的功能)。

固然,这里咱们讨论的是不加锁读:此时的 next-key lock 并非真的加锁,只是为读取的数据增长了标记(标记内容包括数据的版本号等);准确起见姑且称之为类 next-key lock 机制。

仍是之前面的例子来讲明:

当事务 A 在 T2 节点第一次读取 0

这样当 T5 时刻再次读取 0

小结:归纳来讲,InnoDB 实现的 RR,经过锁机制、数据的隐藏列、undo log 和类 next-key lock,实现了必定程度的隔离性,能够知足大多数场景的须要。

不过须要说明的是,RR 虽然避免了幻读问题,可是毕竟不是 Serializable,不能保证彻底的隔离。

下面是一个例子,你们能够本身验证一下:

一致性

基本概念

一致性是指事务执行结束后,数据库的完整性约束没有被破坏,事务执行的先后都是合法的数据状态。

数据库的完整性约束包括但不限于:

  • 实体完整性(如行的主键存在且惟一)
  • 列完整性(如字段的类型、大小、长度要符合要求)
  • 外键约束
  • 用户自定义完整性(如转帐先后,两个帐户余额的和应该不变)

实现

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

实现一致性的措施包括:

  • 保证原子性、持久性和隔离性,若是这些特性没法保证,事务的一致性也没法保证。
  • 数据库自己提供保障,例如不容许向整形列插入字符串值、字符串长度不能超过列的限制等。
  • 应用层面进行保障,例如若是转帐操做只扣除转帐者的余额,而没有增长接收者的余额,不管数据库实现的多么完美,也没法保证状态的一致。

总结

下面总结一下 ACID 特性及其实现原理:

  • 原子性:语句要么全执行,要么全不执行,是事务最核心的特性。事务自己就是以原子性来定义的;实现主要基于 undo log。
  • 持久性:保证事务提交后不会由于宕机等缘由致使数据丢失;实现主要基于 redo log。
  • 隔离性:保证事务执行尽量不受其余事务影响;InnoDB 默认的隔离级别是 RR,RR 的实现主要基于锁机制、数据的隐藏列、undo log 和类 next-key lock 机制。
  • 一致性:事务追求的最终目标,一致性的实现既须要数据库层面的保障,也须要应用层面的保障。

感兴趣的能够本身来个人Java架构群,能够获取免费的学习资料,群号:855801563对Java技术,架构技术感兴趣的同窗,欢迎加群,一块儿学习,相互讨论。

相关文章
相关标签/搜索