数据库事务的四大特性,分别是原子性、一致性、隔离性和持久性,简称为ACID。本文是对四大特性及它们在MySQL中的实现原理的介绍。
原子性
原子性是指一个事务是一个不可分割的工作单位,其中的操作要么都做,要么都不做。
如果事务中一个sql语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态。因此实现原子性的关键,是当事务回滚时能够撤销所有已经成功执行的sql语句。
首先我们要简单了解InnoDB存储引擎提供的两种事务日志:redo log(重做日志)和undo log(回滚日志)。redo log用于保证事务持久性;undo log则是事务原子性和隔离性实现的基础。
InnoDB实现回滚,靠的是undo log:当事务对数据库进行修改时,InnoDB会生成对应的undo log;如果事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo 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宕机而丢失,从而满足了持久性要求。
隔离性
隔离性是指,事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
隔离性追求的是并发情形下事务之间互不干扰,这与多线程程序的执行过程类似。简单起见,我们仅考虑最简单的读操作和写操作(暂时不考虑带锁读等特殊操作),那么隔离性的探讨,主要可以分为两个方面:
(一个事务)写操作对(另一个事务)写操作的影响:锁机制保证隔离性
(一个事务)写操作对(另一个事务)读操作的影响:MVCC(多版本并发控制)保证隔离性。
如果一个事务A在执行写操作时,如果遇到另一个事务B想要执行写操作,那么B的写操作是会被阻塞的,至于锁住的是数据行还是整个表,这要看隔离级别(级别越高越安全,但是效率也越低);而事务B如果想要执行的是读操作,它将被允许通过MVCC提供的数据版本信息来读数据。
一致性
一致性是指事务执行结束后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。数据库的完整性约束包括但不限于:实体完整性(如行的主键存在且唯一)、列完整性(如字段的类型、大小、长度要符合要求)、外键约束、用户自定义完整性(如转账前后,两个账户余额的和应该不变)。
我对一致性的理解是:数据库能正确地执行事务。数据库本身的完整性约束不会被破坏,事务的逻辑能够成功地执行并对数据产生正确的影响。
实现一致性的措施包括:
保证原子性、持久性和隔离性,如果这些特性无法保证,事务的一致性也无法保证
数据库本身提供保障,例如不允许向整形列插入字符串值、字符串长度不能超过列的限制等
应用层面进行保障,例如如果转账操作只扣除转账者的余额,而没有增加接收者的余额。