本文讲解MySQL事务的实现原理和ACID特性。本文将首先介绍MySQL事务相关的基本概念,然后介绍事务的ACID特性,并分析其实现原理。MySQL博大精深,文章疏漏在所难免。欢迎批评指正。MySQL事务基本概念事务(Transaction)是访问和更新数据库的程序执行单元;一个事务可能包含一个或多个SQL语句,这些语句要么被执行,要么不被执行。MySQL作为关系型数据库,支持事务,本文基于MySQL5.6。让我们首先回顾一下MySQL事务的基础知识。逻辑架构和存储引擎如上图所示,MySQL服务器的逻辑架构从上到下分为三层:第一层:处理客户端连接、授权和认证等第二层:服务端层,负责解析、优化、缓存和实现查询语句的内置函数和存储过程。第三层:存储引擎,负责MySQL中数据的存储和提取。MySQL中的服务器层不管理事务,事务由存储引擎实现。MySQL中支持事务的存储引擎有InnoDB、NDBCluster等,其中InnoDB应用最为广泛;其他存储引擎不支持事务,如MyIsam、Memory等。如无特殊说明,以下内容均基于InnoDB。典型的MySQL事务的提交和回滚如下:starttransaction;...#oneormoresqlstatementscommit;其中starttransaction标记事务的开始,commit提交事务,并将执行结果写入数据库。如果sql语句执行有问题,会调用rollback回滚所有成功执行的sql语句。当然你也可以直接在事务中使用rollback语句来回滚。AutocommitMySQL默认采用autocommit模式,如下:在autocommit模式下,如果没有starttransaction显式启动一个事务,那么每条sql语句都会被当作一个事务来执行commit操作。可以通过以下方式关闭自动提交;需要注意的是,autocommit参数是特定于连接的,如果在一个连接中修改了该参数,不会影响其他连接。如果关闭autocommit,所有sql语句都在一个事务中,直到执行commit或rollback,事务结束,同时启动另一个事务。特殊操作在MySQL中,有一些特殊的命令。如果在事务中执行这些命令,会立即强制commit提交事务;比如DDL语句(createtable/droptable/alter/table),locktables语句等等。但是,常用的选择、插入、更新和删除命令不会强制提交事务。ACID特性ACID是衡量事务的四个特性:原子性(Atomicity,或indivisibility)一致性(Isolation)持久性(Durability)根据严格的标准,只有同时满足ACID特性。;然而在各大数据库厂商的实现中,真正满足ACID的事务却寥寥无几。比如MySQL的NDBCluster事务不满足持久化和隔离;InnoDB默认的事务隔离级别是repeatableread,不满足隔离;Oracle默认的事务隔离级别是READCOMMITTED,不满足隔离...所以ACID与其说是一个事务必须满足的条件,不如说是衡量一个事务的四个维度。下面将详细介绍ACID的特性及其实现原理。为了便于理解,介绍的顺序并不严格按照A-C-I-D。ACID特性及其实现原则原子性定义原子性是指一个事务是一个不可分割的工作单元,在这个单元中要么执行所有操作,要么不执行任何操作。如果事务中的一条sql语句执行失败,执行的语句也必须回滚,数据库恢复到事务前的状态。实现原理:undolog在讲解原子性原理之前,先介绍一下MySQL的事务日志。MySQL日志有很多种,比如二进制日志、错误日志、查询日志、慢查询日志等。此外,InnoDB存储引擎还提供了两种事务日志:重做日志(redolog)和撤消日志(rollbacklog),其中重做日志用于保证事务持久化;undolog是事务原子性和隔离性的基础。我们来谈谈撤消日志。实现原子性的关键是在事务回滚时能够撤销所有成功执行的sql语句。InnoDB实现回滚,依赖于undolog:当事务修改数据库时,InnoDB会产生相应的undolog。如果事务执行失败或者调用rollback,导致事务回滚,可以利用undolog中的信息将数据回滚到修改前的状态。undolog是逻辑日志,记录了sql执行相关的信息。当发生回滚时,InnoDB会根据undolog的内容做与之前相反的工作:对于每一次insert,回滚时都会执行delete。对于每个删除,在回滚时执行插入。对于每次更新,回滚时都会执行反向更新,将数据改回来。以update操作为例:事务执行update时,生成的undolog中会包含被修改行的主键(从而知道哪些行被修改了)、哪些列被修改了、这些列修改前后的值。此信息可用于在滚动时将数据恢复到更新前的状态。持久性定义持久性意味着一旦事务被提交,它对数据库的改变应该是永久性的。随后的其他操作或故障不应对其产生任何影响。实现原理:redolog,redolog和undolog都属于InnoDB的事务日志。先说说redolog存在的背景。InnoDB作为MySQL的存储引擎,将数据存储在磁盘上,但是如果每次读写数据都需要磁盘IO,效率会很低。为此,InnoDB提供了缓存(BufferPool)。BufferPool包含一些数据页在磁盘中的映射,作为访问数据库的缓冲区:从数据库中读取数据时,会先从BufferPool中读取。如果Buffer不在Pool中,则会从磁盘中读取并放入BufferPool中。向数据库写入数据时,会先写入BufferPool,BufferPool中修改的数据会周期性刷新到磁盘(这个过程称为flushing)。BufferPool的使用大大提高了读写数据的效率,但是也带来了新的问题:如果MySQL宕机了,此时在BufferPool中修改的数据还没有刷新到磁盘,就会导致数据丢失,事务持久性得不到保证。因此引入了redolog来解决这个问题:当修改数据时,除了修改BufferPool中的数据外,还会将操作记录到redolog中;当事务提交时,会调用fsync接口刷新redolog。如果MySQL宕机,重启时可以读取redolog中的数据来恢复数据库。重做日志使用WAL(Write-aheadlogging,预写日志记录)。所有修改先写入日志,再更新到BufferPool,保证数据不会因为MySQL宕机而丢失,满足持久化需求。既然redolog在事务提交时也需要将日志写入磁盘,那为什么比直接将BufferPool中的修改数据写入磁盘(即刷脏)更快呢?主要有两个原因:刷脏是随机IO,因为每次修改的数据位置是随机的,但是写redolog是append操作,属于顺序IO。dirtying的单位是数据页(Page)。MySQL默认页面大小为16KB,对一个Page的小修改必须写到整个页面;而redolog只包含真正需要写入的部分,大大减少了无效IO。.Redolog和binlog我们知道MySQL中也有binlog(二进制日志),同样可以记录写操作,用于数据恢复,但是两者有本质的区别。作用不同:redolog用于崩溃恢复,保证MySQL宕机不影响持久化;binlog用于时间点恢复,保证服务器可以根据时间点恢复数据,同时binlog也用于master的copyfrom。层次不同:redolog由InnoDB存储引擎实现,binlog由MySQLserver层实现(请参考文章前面MySQL逻辑架构介绍),同时支持InnoDB和其他存储引擎.内容不同:重做日志是物理日志,内容以磁盘的页为单位。binlog是逻辑日志,内容是一行sql。写入时机不同:redolog的写入时机比较多样。前面提到,当事务提交时,会调用fsync刷新redolog;这是默认策略,修改innodb_flush_log_at_trx_commit参数可以改变策略,但不保证事务的持久性。除了事务提交时,还有其他机会刷新磁盘:比如master线程每秒刷新一次redolog。事务提交时写入二进制日志。隔离的定义不同于原子性和持久性,后者侧重于对事务本身的研究。隔离研究的是不同事务之间的交互。隔离是指事务内的操作与其他事务隔离,并发执行的事务之间不能相互干扰。严格隔离对应事务隔离级别中的Serializable,但出于性能考虑,在实际应用中很少使用序列化。隔离的追求是事务在并发情况下互不干扰。为了简单起见,我们只考虑最简单的读写操作(暂时不考虑readwithlock等特殊操作)。那么关于隔离的讨论主要可以分为两个方面:(一个事务)写操作对(另一个事务)写操作的影响:锁机制保证隔离。(一个事务)写操作对(另一个事务)读操作的影响:MVCC保证隔离。锁机制首先看两个事务写操作的交互。隔离要求一次只能有一个事务写入数据,InnoDB通过锁机制来保证这一点。锁机制的基本原理可以概括为:事务在修改数据之前,需要先获取对应的锁。获取到锁后,事务就可以修改数据了。在事务操作过程中,这部分数据是被锁定的。如果其他事务需要修改数据,则需要等待当前事务提交或回滚释放锁。行锁和表锁:按照粒度,锁可以分为表锁、行锁和介于两者之间的其他锁。表锁在操作数据时会锁住整个表,并发性能差;行锁只锁住需要操作的数据,并发性能好。但是由于加锁本身是消耗资源的(获取锁、检查锁、释放锁等都是消耗资源的),所以在加锁数据很多的时候使用表锁可以节省很多资源。MySQL中不同的存储引擎支持不同的锁。比如MyIsam只支持表锁,而InnoDB同时支持表锁和行锁。出于性能原因,大多数情况下使用行锁。如何查看锁信息?InnoDB中查看锁的方式有很多种,例如:select*frominformation_schema.innodb_locks;#锁的概览showengineinnodbstatus;#InnoDB的整体状态,包括锁我们来看一个例子:#在事务ExecuteinA:starttransaction;updateaccountSETbalance=1000whereid=1;在事务B中执行:starttransaction;updateaccountSETbalance=2000whereid=1;此时查看锁的状态:showengineinnodbstatus查看锁的相关部分:可以通过上面的命令查看事务24052和24053的锁是否被占用;其中lock_type是RECORD,表示锁是行锁(记录锁);lock_mode为X,表示排它锁(写锁)。除了独占锁(写锁),MySQL还有共享锁(读锁)的概念。由于本文重点介绍MySQL事务的实现原理,所以锁的介绍到此结束。在介绍了写操作之间的交互之后,下面讨论写操作对读操作的影响。脏读、不可重复读和幻读首先我们来看一下并发条件下读操作可能存在的三类问题。①脏读:在当前事务(A)中可以读到其他事务(B)未提交的数据(脏数据)。这种现象就是脏读。示例如下(以账户余额表为例):②不可重复读:在事务A中,连续两次读取相同的数据,两次读取的结果不同。这种现象称为不可重复读。脏读和不可重复读的区别在于前者读取其他事务未提交的数据,后者读取其他事务的已提交数据。示例如下:③幻读:事务A中,根据某个条件对数据库进行了两次查询,两次查询的结果个数不同。这种现象称为幻读。不可重复读和幻读的区别可以大致理解为:前者是指数据发生了变化,后者是指数据的行数发生了变化。举例如下:事务隔离级别sql标准定义了四种隔离级别,并规定了每种隔离级别下是否存在上述问题。一般来说,隔离级别越低,系统开销越低,可以支持的并发越高,但隔离性越差。隔离级别与读问题的关系如下:在实际应用中,readuncommitted会在并发时造成很多问题,但相对于其他隔离级别的性能提升非常有限,所以较少使用。Serializable强制事务序列化,并发效率很低。只有在数据一致性要求极高,不能接受并发的情况下才会用到,所以用的比较少。因此,在大多数数据库系统中,默认的隔离级别是readcommitted(如Oracle)或repeatableread(以下简称RR)。可以使用如下两条命令分别查看全局隔离级别和本次会话的隔离级别:InnoDB默认的隔离级别是RR,后面会介绍RR。需要注意的是,在SQL标准中,RR无法避免幻读问题,而InnoDB实现的RR却避免了幻读问题。MVCCRR解决脏读、不可重复读、幻读等问题,使用MVCC:MVCC全称Multi-VersionConcurrencyControl,是一种多版本并发控制协议。下面的例子很好的体现了MVCC的特点:同时,不同事务读取的数据可能是不同的(即多个版本)——在T5时刻,事务A和事务C可以读取到不同版本的数据。MVCC最大的优点就是读不加锁,所以读写不冲突,并发性能好。InnoDB实现了MVCC,多个版本的数据可以共存,主要靠数据的隐藏列(也叫标记位)和undolog。数据的隐藏列包括数据行的版本号、删除时间、指向undolog的指针等。MySQL在读取数据时,可以通过隐藏列判断是否需要回滚,找到回滚需要的undolog,从而实现MVCC;隐藏列的详细格式不再展开。下面就上面提到的几个问题进行解释。①脏读当事务A在T3时间节点读取zhangsan的余额时,会发现数据已经被其他事务修改,状态为未提交。此时事务A读取到最新的数据后,根据数据的undolog进行回滚操作,得到事务B修改前的数据,从而避免了脏读。②不可重复读事务A在节点T2第一次读取数据时,会记录数据的版本号(数据的版本号以行为单位记录),假设版本号为1;当事务B提交时,该行记录的版本号增加,假设版本号为2。当事务A在T5再次读取数据时,发现数据的版本号(2)大于版本号(1)第一次读取时记录,所以根据undolog进行回滚操作,版本号为1的时间敏感数据,从而实现可重复读取。③幻读InnoDB实现的RR通过next-keylock机制避免了幻读。next-keylock是行锁的一种,相当于记录锁(recordlock)+间隙锁(gaplock);它的特点是不仅会锁住记录本身(记录锁的功能),还会锁住一个范围(间隙锁)锁功能)。当然,我们这里说的是不加锁读取:此时的next-key锁并不是真正的加锁,只是给读取的数据加上一个标记(标记的内容包括数据的版本号,ETC。);为了准确起见,我们称它为next-key锁机制。还是用前面的例子来说明:当事务A在节点T2第一次读0,然后在T5再次读0,并且类next-key锁,实现一定程度的隔离,可以满足大部分场景的需求。不过需要注意的是,RR虽然避免了幻读问题,但毕竟不是Serializable,不能保证完全隔离。下面是一个例子,大家可以自己验证一下:一致性的基本概念一致性是指事务执行后,数据库的完整性约束没有被破坏,事务执行前后的数据状态为合法的。数据库的完整性约束包括但不限于:实体完整性(如行的主键存在且唯一)列完整性(如字段的类型、大小、长度必须满足要求)外部keyconstraint自定义完整性(比如转账前后,两个账户的余额之和应该保持不变)可以说一致性是交易追求的最终目标:上面说的原子性、持久化、隔离性都是为了保证数据库状态的一致性。另外,一致性的实现除了数据库层面的保障外,还需要应用层面的保障。实现一致性的措施包括:保证原子性、持久性和隔离性。如果不能保证这些特性,那么事务的一致性也无法保证。数据库本身提供了保证,例如不允许将字符串值插入整型列,字符串的长度不能超过列的限制等应用层面的保证。例如,如果转账操作只扣除转账方的余额,不增加接收方的余额,那么无论数据库多么完善,也无法保证状态的一致性。总结下面总结一下ACID的特性及其实现原理:原子性:语句要么全部执行,要么根本不执行,是事务的核心特性。事务本身是根据原子性定义的;该实现主要基于撤消日志。持久性:保证事务提交后不会因为宕机等原因导致数据丢失;实现主要基于redolog。隔离性:尽可能保证事务执行不受其他事务的影响;InnoDB默认的隔离级别是RR,RR的实现主要基于锁机制、数据隐藏列、undolog和next-key锁机制。一致性:交易追求的最终目标。一致性的实现既需要数据库层面的保证,也需要应用层面的保证。参考文献:《MySQL 技术内幕:InnoDB 存储引擎》《高性能 MySQL》?https://dev.mysql.com/doc/refman/5.6/en/glossary.html#glos_acidhttps://dev.mysql.com/doc/refman/5.6/en/innodb-next-key-locking.htmlhttp://blog.sina.com.cn/s/blog_499740cb0100ugs7.htmlhttps://mp.weixin.qq.com/s/2dwGBTmu_da2x-HiHlN0vwhttp://www.cnblogs.com/chenpingzhao/p/5065316.htmlhttps://juejin.im/entry/5ba0a254e51d450e735e4a1fhttp://hedengcheng.com/?p=771【原创稿件,合作转载请注明原作者及出处.com网站】
