mysql事务的实现原理

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

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

(1)第一层:处理客户端链接、受权认证等。sql

(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语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态。 简单来讲,就是

实现原理 在说明原子性原理以前,首先介绍一下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,不能保证彻底的隔离,下面是一个例子,你们能够本身验证一下。

一致性

基本概念

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

实现

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

实现一致性的措施包括:

  • 保证原子性、持久性和隔离性,若是这些特性没法保证,事务的一致性也没法保证

  • 数据库自己提供保障,例如不容许向整形列插入字符串值、字符串长度不能超过列的限制等

  • 应用层面进行保障,例如若是转帐操做只扣除转帐者的余额,而没有增长接收者的余额,不管数据库实现的多么完美,也没法保证状态的一致