事务(Transaction)是数据库区别于文件系统的重要特性之一。在文件系统中,如果正在写文件,但是操作系统突然崩溃了,这个文件就很可能被破坏。当然有一些机制可以把文件恢复到某个时间点。不过,如果需要保证两个文件同步,这些文件系统可能就显得无能为力了。例如,在需要更新两个文件时,更新完一个文件后,在更新完第二个文件之前系统重启了,就会有两个不同步的文件。(假设有两个数据库中有两个文件:用户信息文件、订单信息文件。在更新完用户信息文件后,更新完订单信息文件前,系统重启了,重启后就会导致,用户信息文件已变更,订单信息文件未变更。)
这正是数据库系统引入事务的主要目的:事务会把数据库从一种一致状态转换为另一种一致状态。在数据库提交工作时,可以确保要么所有修改都已经保存了,要么所有修改都不保存。
InnoDB 存储引擎中的事务完全符合 ACID 的特性。ACID 是以下 4 个词的缩写:
-
原子性(atomicity)
-
一致性(consistency)
-
隔离性(isolation)
-
持久性(durability)
本文主要关注事务的原子性,并说明怎样正确使用事务及编写正确的事务应用程序,避免在事务方面养成一些不好的习惯。
事务概述
在事务中的操作,要么都做修改,要么都不做,这就是事务的目的,也是事务模型区别与文件系统的重要特征之一。
扩展:
理论上说,事务有着极其严格的定义,它必须同时满足四个特性,即通常所说的事务的 ACID 特性。值得注意的是,虽然理论上定义了严格的事务要求,但是数据库厂商出于各种目的,并没有严格去满足事务的 ACID 标准。例如,对于 MySQL 的 NDB Cluster 引擎来说,虽然其支持事务,但是不满足 D 的要求,即持久性的要求。对于 Oracle 数据库来说,其默认的事务隔离级别为 READ COMMITTED,不满足 I 的要求,即隔离性的要求。虽然在大多数的情况下,这并不会导致严重的结果,甚至可能还会带来性能的提升,但是用户首先需要知道严谨的事务标准,并在实际的生产应用中避免可能存在的潜在问题。
对于 InnoDB 存储引擎而言,其默认的事务隔离级别为 READ REPEATABLE,完全遵循和满足事务的 ACID 特性。
1)A(Atomicity),原子性:事务要么执行成功,要么执行失败。
-
保证事务原子性的难点:
-
大事务回滚 【redo log+undo log】
-
故障恢复,只有一半修改数据刷到磁盘的数据如何恢复 【redo log】
-
2)C(consistecy),一致性:事务将数据库从一种状态转变为另一个状态。例如,一个表中的某一列数据进行了修改,则和该列相关的所有表都需要修改。
-
保证事务一致性的难点:
-
多版本并发控制【undo log】
-
只有实现了事务的原子性、隔离性和持久性才能保证事务的一致性
-
3)I(isolation),隔离性:隔离性还有其他的称呼,如并发控制、可串行化、锁等。该事务提交前对其他事务都不可见,通常使用锁来实现
-
保证事务隔离性的难点:
-
多版本并发控制【undo log】
-
保证事务的顺序执行【redo log】
-
4)D(durability),持久性:事务一旦提交,其结果就是永久性的,即使发生宕机等故障,数据库也能将数据恢复。但若不是数据库本身发生故障,而是一些外部的原因,如RAID卡损坏、自然灾害等原因导致数据库发生问题,那么所有提交的数据可能都会丢失。因此持久性保证事务系统的高可靠性(High Reliability),而不是高可用性(High Availability)。对于高可用性的实现,事务本身并不能保证,需要一些系统共同配合来完成。
- 保证事务持久性的难点:宕机或异常数据不丢失【redo log】
事务分类
从事务理论的角度来说,可以把事务分为以下几种类型:
-
扁平事务(Flat Transactions)
-
带有保存点的扁平事务(Flat Transactions with Savepoints)
-
链事务(Chained Transactions)
-
嵌套事务(Nested Transactions)
-
分布式事务(Distributed Transactions)
扁平事务
扁平事务(Flat Transactions) 是事务类型中最简单的一种,但在实际生产环境中,这可能是使用最为频繁的事务。在扁平事务中,所有操作都处于同一层次,其由 begin work 开始,由 commit work 或 rollback work 结束,其间的操作都是原子的,要么都执行,要么都回滚。因此扁平事务是应用程序称为原子操作的基本组成模块。下图显示了扁平事务的三种不同结果。
带有保存点的扁平事务
带有保存点的扁平事务(Flat Transactions with Savepoints) ,除了支持扁平事务支持的操作外,允许在事务执行过程中回滚到同一事物中较早的一个状态(事务内回滚)。这是因为某些事务可能在执行过程中出现的错误并不会导致所有的操作都无效,放弃整个事务不合乎要求,开销也太大。
保存点(Savepoint)用来通知系统记住事务当前的状态,以便当之后发生错误时,事务能回到保存点当时的状态。
对于扁平事务来说,其隐式地设置了一个保存点(起始点)。然而在整个事务中,只有这一个保存点,因此,回滚只能回滚到事务开始时的状态。保存点用 save work 函数来建立,通知系统记录当前的处理状态。当出现问题时,保存点能用作内部的重启动点,根据应用逻辑,决定是回到最近一个保存点还是其他更早的保存点。下图显示了在事务中使用保存点的情况。
上图显示了如何在事务中使用保存点。灰色背景部分的操作表示由 rollback work 而导致部分回滚,实际并没有执行的操作。当用 begin work 开启一个事务时,隐式地包含了一个保存点,当事务通过 rollback work:2 发出部分回滚命令时,事务回滚到保存点 2,接着依次执行,并再次执行到 rollback work:7 ,直到最后的 commit work 操作,这时表示事务结束,除灰色阴影部分的操作外,其余操作都已经执行,并且提交。
注:另一点需要注意的是,保存点在事务内部是递增的,这从上图中也能看出。有人可能会想,返回保存点 2 以后,下一个保存点可以为 3,因为之前的工作都终止了。然而新的保存点编号为 5,这意味着 rollback 不影响保存点的计数,并且单调递增的编号能保持事务执行的整个历史过程。
分布式事务
分布式事务(Distributed Transactions),通常是一个在分布式环境下运行的扁平事务,因此需要根据数据所在位置访问网络中的不同节点。
假设一个用户在 ATM 机进行银行的转账操作,例如持卡人从招商银行的储蓄卡转账 10000 元到工商银行的储蓄卡。在这种情况下,可以将 ATM 机视为节点 A,招商银行的后台数据库视为节点 B,工商银行的后台数据库视为 C,这个转账操作可分解为以下的步骤:
-
节点 A 发出转账命令。
-
节点 B 执行储蓄卡中的余额值减去 10000。
-
节点 C 执行储蓄卡中的余额值加上 10000。
-
节点 A 通知用户操作完成或节点 A 通知用户操作失败。
这里需要使用分布式事务,因为节点 A 不能通过调用一台数据库就完成任务。其需要访问网络中的两个节点数据库,而在每个节点的数据库执行的事务操作又都是扁平的。
对于分布式事务,其同样需要满足 ACID 特性,要么都发生,要么都失效。对于上述的例子,如果步骤 2、3中任何一个操作失败,都会导致整个分布式事务回滚。若非这样,结果会非常可怕。
对于 InnoDB 存储引擎来说,其支持扁平事务、带有保存点的事务、链事务、分布式事务。
注:链事务(Chained Transactions)、嵌套事务(Nested Transactions)这里不再过多介绍,若想了解,请参考:MySQL技术内幕 InnoDB存储引擎 第2版