事务是访问数据库的程序执行单元;事务中可能包含一个或多个 SQL
语句,这些语句要么都执行,要么都不执行。事务的定义有几点需要解释下
SQL
语句,这些操作构成一个逻辑上的整体转账是生活中常见的操作,比如从 A
账户转账 100
元到 B
账号。站在用户角度而言,这是一个逻辑上的单一操作,然而在数据库系统中,至少会分成两个步骤来完成
A
账户的金额减少 100
元B
账户的金额增加 100
元A
账户上的钱减少了 100
元,但是第二步执行失败或者未执行便发生系统崩溃,导致 B
账户并没有相应增加 100
元B
账户,由于同时对 B
账户进行操作,导致 B
账户金额出现异常为了便于解决这些问题,需要引入数据库事务的概念
Atomicity
):事务中的所有操作作为一个整体像原子一样不可分割,要么全部成功,要么全部失败Consistency
):事务的执行结果必须使数据库从一个一致性状态到另一个一致性状态Isolation
):并发执行的事务不会相互影响,其对数据库的影响和它们串行执行时一样。比如多个用户同时往一个账户转账,最后账户的结果应该和他们按先后次序转账的结果一样Durability
):事务一旦提交,其对数据库的更新就是持久的。任何事务或系统故障都不会导致数据丢失按照严格的标准,只有同时满足 ACID
特性才是事务;但是在各大数据库厂商的实现中,真正满足 ACID
的事务少之又少。例如 MySQL
的 NDB Cluster
事务不满足持久性和隔离性;InnoDB
默认事务隔离级别是可重复读,不满足隔离性;Oracle
默认的事务隔离级别为 READ COMMITTED
,不满足隔离性……因此与其说 ACID
是事务必须满足的条件,不如说它们是衡量事务的四个维度
在说明原子性原理之前,首先介绍一下 MySQL
的事务日志。MySQL
的日志有很多种,如二进制日志、错误日志、查询日志、慢查询日志等,此外 InnoDB
存储引擎还提供了两种事务日志:redo log
([riːˈduː]
重做日志)和 undo log
([ʌnˈduː]
回滚日志)。其中 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
存在的背景: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
MySQL
默认页大小是16KB
,一个数据页上一个小修改都要整页写入;而 redo log
中只包含真正需要写入的部分,无效 IO
大大减少redo log
与 bin log
在 MySQL
中还存在 bin log
(二进制日志)也可以记录写操作并用于数据的恢复,但二者是有着根本的不同的
redo log
是用于事故恢复的,保证 MySQL
宕机也不会影响持久性;bin log
是用于时间点恢复的,保证服务器可以基于时间点恢复数据,此外 bin log
还用于主从复制redo log
是 InnoDB
存储引擎实现的,而 bin log
是 MySQL
的服务器层实现的,同时支持 InnoDB
和其他存储引擎bin log
在事务提交时写入;redo log
的写入时机相对多元与原子性、持久性侧重于研究事务本身不同,隔离性研究的是不同事务之间的相互影响。隔离性追求的是并发情形下事务之间互不干扰。简单起见,我们主要考虑最简单的读操作和写操作(加锁读等特殊读操作会特殊说明),那么隔离性的探讨,主要可以分为两个方面
MVCC
保证隔离性隔离性要求同一时刻只能有一个事务对数据进行写操作,InnoDB
通过锁机制来保证这一点。锁机制的基本原理可以概括为:事务在修改数据之前,需要先获得相应的锁;获得锁之后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁
MySQL
锁的分类
本文重点是 MySQL
事务的实现原理,因此对锁的介绍到此为止,详情可参考 这里
数据库在并发情况下,读操作可能存在的三类问题
A
可以读到其他事务 B
未提交的数据(脏数据),这种现象是脏读A
先后两次读取同一个数据,两次读取的结果不一样,这种现象称为不可重复读。脏读与不可重复读的区别在于:前者读到的是其他事务未提交的数据,后者读到的是其他事务已提交的数据A
按照某个条件先后两次查询数据库,两次查询结果的条数不同,这种现象称为幻读。不可重复读与幻读的区别可以通俗的理解为:前者是数据变了,后者是数据的行数变了一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差。隔离级别与读问题的关系如下
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 | 会 | 会 | 会 |
读已提交 | 不会 | 会 | 会 |
可重复读 | 不会 | 不会 | 会 |
串行化 | 不会 | 不会 | 不会 |
在实际应用中,读未提交在并发时会导致很多问题,而性能相对于其他隔离级别提高却很有限,因此使用较少。可串行化强制事务串行,并发效率很低,只有当对数据一致性要求极高且可以接受没有并发时使用,因此使用也较少。因此在大多数数据库系统中,默认的隔离级别是读已提交(如 Oracle
)或可重复读(如 MySQL
)
MVCC
MVCC
全称 Multi-Version Concurrency Control
,即多版本的并发控制协议。下面的例子很好的体现了 MVCC
的特点:在同一时刻,不同的事务读取到的数据可能是不同的(即多版本)——在 T5
时刻,事务 A
和事务 C
可以读取到不同版本的数据
MVCC
最大的优点是读不加锁,因此读写不冲突,并发性能好。InnoDB
实现 MVCC
,多个版本的数据可以共存。下面以可重复读隔离级别为例,结合前文提到的几个问题分别说明
当事务 A
在 T3
时刻读取 zhangsan
的余额前,会生成 ReadView
,由于此时事务 B
没有提交仍然活跃,因此其事务 id
一定在 ReadView
的 rw_trx_ids
中,因此根据前面介绍的规则,事务 B
的修改对 ReadView
不可见。接下来,事务 A
根据指针指向的 undo log
查询上一版本的数据,得到 zhangsan
的余额为 100
。这样事务 A
就避免了脏读
当事务 A
在 T2
时刻读取 zhangsan
的余额前,会生成 ReadView
。此时事务 B
分两种情况讨论,一种是如图中所示,事务已经开始但没有提交,此时其事务 id
在 ReadView
的 rw_trx_ids
中;一种是事务 B
还没有开始,此时其事务 id
大于等于 ReadView
的 low_limit_id
。无论是哪种情况,根据前面介绍的规则,事务 B
的修改对 ReadView
都不可见
当事务 A
在 T5
时刻再次读取 zhangsan
的余额时,会根据 T2
时刻生成的 ReadView
对数据的可见性进行判断,从而判断出事务 B
的修改不可见;因此事务 A
根据指针指向的 undo log
查询上一版本的数据,得到 zhangsan
的余额为 100
,从而避免了不可重复读
InnoDB
实现的可重复读,通过锁机制、MVCC
等,实现了一定程度的隔离性,可以满足大多数场景的需要;但是,可重复读虽然避免了幻读问题,但是毕竟不是 Serializable
(串行化),不能保证完全的隔离
可以说,一致性是事务追求的最终目标:前面提到的原子性、持久性和隔离性,都是为了保证数据库状态的一致性。此外,除了数据库层面的保障,一致性的实现也需要应用层面进行保障
下面总结一下 ACID
特性及其实现原理
undo log
redo log
InnoDB
默认的隔离级别是可重复读,它的实现主要基于锁机制、MVCC