什么是ACID
ACID是衡量事务的四个特性:
原⼦性(Atomicity,或称不可分割性)
⼀致性(Consistency)
隔离性(Isolation)
持久性(Durability)
原⼦性:原⼦性是指⼀个事务是⼀个不可分割的⼯作单位,其中的操作要么都做,要么都不做;如果事务中⼀个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的内容做与之前相反的⼯作:对于每个inrt,回滚时会执⾏delete;对于每个delete,回滚时会执⾏inrt;对于每个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中修改的数据写⼊磁盘(即刷脏)要快呢?主要有以下两⽅⾯的原因:
(1)刷脏是随机IO,因为每次修改的数据位置随机,但写redo log是追加操作,属于顺序IO。
(2)刷脏是以数据页(Page)为单位的,MySQL默认页⼤⼩是16KB,⼀个Page上⼀个⼩修改都要整页写⼊;⽽redo log中只包含真正需
要写⼊的部分,⽆效IO⼤⼤减少。
隔离性:与原⼦性、持久性侧重于研究事务本⾝不同,隔离性研究的是不同事务之间的相互影响。隔离性是指,事务内部的操作与其他事务是隔离的,并发执⾏的各个事务之间不能互相⼲扰。严格的隔离性,对应了事务隔离级别中的Serializable (可串⾏化),但实际应⽤中出于性能⽅⾯的考虑很少会使⽤可串⾏化。
隔离性追求的是并发情形下事务之间互不⼲扰。简单起见,我们仅考虑最简单的读操作和写操作(暂时不考虑带锁读等特殊操作),那么隔离性的探讨,主要可以分为两个⽅⾯:
(⼀个事务)写操作对(另⼀个事务)写操作的影响:锁机制保证隔离性
(⼀个事务)写操作对(另⼀个事务)读操作的影响:MVCC保证隔离性
2. 锁机制
⾸先来看两个事务的写操作之间的相互影响。隔离性要求同⼀时刻只能有⼀个事务对数据进⾏写操作,InnoDB通过锁机制来保证这⼀点。
锁机制的基本原理可以概括为:事务在修改数据之前,需要先获得相应的锁;获得锁之后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁。
⾏锁与表锁
按照粒度,锁可以分为表锁、⾏锁以及其他位于⼆者之间的锁。表锁在操作数据时会锁定整张表,并发性能较差;⾏锁则只锁定需要操作的数据,并发性能好。但是由于加锁本⾝需要消耗资源(获得锁、检查锁、释放锁等都需要消耗资源),因此在锁定数据较多情况下使⽤表锁可以节省⼤量资源。MySQL中不同的存储引擎⽀持的锁是不⼀样的,例如MyIsam只⽀持表锁,⽽InnoDB同时⽀持表锁和⾏锁,且出于性能考虑,绝⼤多数情况下使⽤的都是⾏锁。
⼀致性:⼀致性是指事务执⾏结束后,数据库的完整性约束没有被破坏,事务执⾏的前后都是合法的数据状态。数据库的完整性约束包括但不限于:实体完整性(如⾏的主键存在且唯⼀)、列完整性(如字段的类型、⼤⼩、长度要符合要求)、外键约束、⽤户⾃定义完整性(如转账前后,两个账户余额的和应该不变)。
实现
可以说,⼀致性是事务追求的最终⽬标:前⾯提到的原⼦性、持久性和隔离性,都是为了保证数据库状态的⼀致性。此外,除了数据库层⾯的保障,⼀致性的实现也需要应⽤层⾯进⾏保障。
实现⼀致性的措施包括:
保证原⼦性、持久性和隔离性,如果这些特性⽆法保证,事务的⼀致性也⽆法保证
数据库本⾝提供保障,例如不允许向整形列插⼊字符串值、字符串长度不能超过列的限制等
应⽤层⾯进⾏保障,例如如果转账操作只扣除转账者的余额,⽽没有增加接收者的余额,⽆论数据库实现的多么完美,也⽆法保证状态的⼀致