1、概述
事务可由一条非常简单的SQL语句组成,也可以由一组复杂的SQL语句组成。事务是访问并更新数据库中各种数据项的一个程序执行单元。在事务中的操作,要么都做修改,要么都不做,这就是事务的目的,也是事务模型区别与文件系统的重要特征之一。
理论上说,事务有着极其严格的定义,它必须同时满足四个特性,通常所说的事务的ACID特性。值得注意的是,虽然理论上定义了严格的事务要求,但是数据库厂商出于各种目的,并没有严格去满足事务的ACID标准。例如,对于 MySQL的NDB Cluster引擎来说,虽然其支持事务,但是不满足D的要求,即持久性的要求。对于 Oracle数据库来说,其默认的事务隔离级别为 READ COMMITTED,不满足I的要求,即隔离性的要求。虽然在大多数的情况下,这并不会导致严重的结果,甚至可能还会带来性能的提升,但是用户首先需要知道严谨的事务标准,并在实际的生产应用中避免可能存在的潜在问题。对于InnoDB存储引擎而言,其默认的事务隔离级别为READ REPEATABLE,完全遵循和满足事务的ACID特性。这里,具体介绍事务的ACID特性,并给出相关概念。
A(Atomicity),原子性 。在计算机系统中,每个人都将原子性视为理所当然。例如在C语言中调用SQRT函数,其要么返回正确的平方根值,要么返回错误的代码,而不会在不可预知的情况下改变任何的数据结构和参数。如果SQRT函数被许多个程序调用,一个程序的返回值也不会是其他程序要计算的平方根。
然而在数据的事务中实现调用操作的原子性,就不是那么理所当然了。例如一个用户在ATM机前取款,假设取款的流程为:
- 登录ATM机平台,验证密码。
- 从远程银行的数据库中,取得账户的信息
- 用户在ATM机上输入欲提取的金额。
- 从远程银行的数据库中,更新账户信息。
- ATM机出款
- 用户取钱。
整个取款的操作过程应该视为原子操作,即要么都做,要么都不做。不能用户钱未从ATM机上取得,但是银行卡上的钱已经被扣除了,相信这是任何人都不能接受的种情况。而通过事物模型,可以保证该操作的原子性。
原子性指整个数据库事务是不可分割的工作单位。只有使事务中所有的数据库操作都执行成功,才算整个事务成功。事务中任何一个SQL语句执行失败,已经执行成功的SQL语句也必须撤销,数据库状态应该退回到执行事务前的状态。
如果事务中的操作都是只读的,要保持原子性是很简单的。一旦发生任何错误,要么重试,要么返回错误代码。因为只读操作不会改变系统中的任何相关部分俱是当事务中的操作需要改变系统中的状态时,例如插入记录或更新记录,那么情况可能就不像只读操作那么简单了。如果操作失败,很有可能引起状态的变化,因此必须要保护系统中并发用户访问受影响的部分数据。
C(consistency),一致性。 一致性指事务将数据库从一种状态转变为下一种一致的状态。在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。例如,在表中有一个字段为姓名,为唯一约束,即在表中姓名不能重复。如果一个事务对姓名字段进行了修改,但是在事务提交或事务操作发生回滚后,表中的姓名变得非唯一了,这就破坏了事务的一致性要求,即事务将数据库从一种状态变为了一种不一致的状态。因此,事务是一致性的单位,如果事务中某个动作失败了,系统可以自动撤销事务—返回初始化的状态。
l(isolation),隔离性。 隔离性还有其他的称呼,如并发控制(concurrency control)、可串行化(serializability)、锁(locking)等。事务的隔离性要求每个读写事务的对象对其他事务的操作对象能相互分离,即该事务提交前对其他事务都不可见,通常这使用锁来实现。当前数据库系统中都提供了一种粒度锁(granular lock)的策略,允许事务仅锁住一个实体对象的子集,以此来提高事务之间的并发度。
D(durability),持久性。 事务一旦提交,其结果就是永久性的。即使发生宕机等故障,数据库也能将数据恢复。需要注意的是,只能从事务本身的角度来保证结果的永久性。例如,在事务提交后,所有的变化都是永久的。即使当数据库因为崩溃而需要恢复时,也能保证恢复后提交的数据都不会丢失。但若不是数据库本身发生故障,而是一些外部的原因,如RAID卡损坏、自然灾害等原因导致数据库发生问题,那么所有提交的数据可能都会丢失。因此持久性保证事务系统的高可靠性(High Reliability),而不是高可用性(High Availability)。对于高可用性的实现,事务本身并不能保证,需要一些系统共同配合来完成。
2、分类
从事务理论的角度来说,可以把事务分为以下几种类型:
- 扁平事务(Flat Transactions)
- 带有保存点的扁平事务(Flat Transactions with Savepoints)
- 链事务(Chained Transactions)
- 嵌套事务(Nested Transactions)
- 分布式事务(Distributed transactions)
扁平事务(Flat Transaction) 是事务类型中最简单的一种,但在实际生产环境中,这可能是使用最为频繁的事务。在扁平事务中,所有操作都处于同一层次,其由 BEGIN WORK开始,由COMMIT WORK或ROLLBACK WORK结束,其间的操作是原子的,要么都执行,要么都回滚。因此扁平事务是应用程序成为原子操作的基本组成模块。
下图显示了扁平事务的三种不同结果。
图中给出了扁平事务的三种情况,同时也给出了在一个典型的事务处理应用中,每个结果大概占用的百分比。再次提醒,扁平事务虽然简单,但在实际生产环境中使用最为频繁。正因为其简单,使用频繁,故每个数据库系统都实现了对扁平事务的支持。
扁平事务的主要限制是不能提交或者回滚事务的某一部分,或分几个步骤提交。下面给出一个扁平事务不足以支持的例子。例如用户在旅行网站上进行自己的旅行度假计划。用户设想从杭州到意大利的佛罗伦萨,这两个城市之间没有直达的班机,需要用户预订并转乘航班,或者需要搭火车等待。用户预订旅行度假的事务为:
BEGIN WORK
S1:预订杭州到上海的高铁
S2:上海浦东国际机场坐飞机,预订去米兰的航班
S3:在米兰转火车前往佛罗伦萨,预订去佛罗伦萨的火车
但是当用户执行到S3时,发现由于飞机到达米兰的时间太晚,已经没有当天的火车。这时用户希望在米兰当地住一晚,第二天出发去佛罗伦萨。这时如果事务为扁平事务,则需要回滚之前S1、S2、S3的三个操作,这个代价就显得有点大。因为当再次进行该事务时,S1、S2的执行计划是不变的。也就是说,如果支持有计划的回滚操作,那么就不需要终止整个事务。因此就出现了带有保存点的扁平事务。
带有保存点的扁平事务(Flat Transactions with Savepoint) ,除了支持扁平事务支持的操作外,允许在事务执行过程中回滚到同一事务中较早的一个状态。这是因为某些事务可能在执行过程中出现的错误并不会导致所有的操作都无效,放弃整个事务不合乎要求,开销也太大。保存点(Savepoint)用来通知系统应该记住事务当前的状态,以便当之后发生错误时,事务能回到保存点当时的状态。
对于扁平的事务来说,其隐式地设置了一个保存点。然而在整个事务中,只有这个保存点,因此,回滚只能回滚到事务开始时的状态。保存点用 SAVE WORK函数来建立,通知系统记录当前的处理状态。当出现问题时,保存点能用作内部的重启动点,根据应用逻辑,决定是回到最近一个保存点还是其他更早的保存点。下图显示了在事务中使用保存点。
上图显示了如何在事务中使用保存点。灰色背景部分的操作表示由ROLLBACK WORK而导致部分回滚,实际并没有执行的操作。当用BEGIN WORK开启一个事务时,隐式地包含了一个保存点,当事务通过 ROLLBACK WORK:2发出部分回滚命令时,事务回滚到保存点2,接着依次执行,并再次执行到 ROLLBACK WORK:7,直到最后的 COMMIT WORK操作,这时表示事务结束,除灰色阴影部分的操作外,其余操作都已经执行,并且提交。
另一点需要注意的是,保存点在事务内部是递增的,这从图7-2中也能看出。有人可能会想,返回保存点2以后,下一个保存点可以为3,因为之前的工作都终止了。然而新的保存点编号为5,这意味着 ROLLBACK不影响保存点的计数,并且单调递增的编号能保持事务执行的整个历史过程,包括在执行过程中想法的改变。
此外,当事务通过 ROLLBACK WORK:2命令发出部分回滚命令时,要记住事务并没有完全被回滚,只是回滚到了保存点2而已。这代表当前事务还是活跃的,如果想要完全回滚事务,还需要再执行命令 ROLLBACK WORK。
链事务(Chained Transaction) 可视为保存点模式的一种变种。带有保存点的扁平事务,当发生系统崩溃时,所有的保存点都将消失,因为其保存点是易失的(volatile),而非持久的(persistent)。这意味着当进行恢复时,事务需要从开始处重新执行,而不能从最近的一个保存点继续执行。
链事务的思想是:在提交一个事务时,释放不需要的数据对象,将必要的处理上下文隐式地传给下一个要开始的事务。注意,提交事务操作和开始下一个事务操作将合并为一个原子操作。这意味着下一个事务将看到上一个事务的结果,就好像在一个事务中进行的一样。下图显示了链事务的工作方式:
链事务与带有保存点的扁平事务不同的是,带有保存点的扁平事务能回滚到任意正确的保存点。而链事务中的回滚仅限于当前事务,即只能恢复到最近一个的保存点。对于锁的处理,两者也不相同。链事务在执行 COMMIT后即释放了当前事务所持有的锁,而带有保存点的扁平事务不影响迄今为止所持有的锁。
嵌套事务(Nested Transaction) 是一个层次结构框架。由一个顶层事务(top level transaction)控制着各个层次的事务。顶层事务之下嵌套的事务被称为子事务(subtransaction),其控制每一个局部的变换。嵌套事务的层次结构如图所示。
下面给出Moss对嵌套事务的定义
- 嵌套事务是由若干事务组成的一棵树,子树既可以是嵌套事务,也可以是扁平事务。
- 处在叶节点的事务是扁平事务。但是每个子事务从根到叶节点的距离可以是不同的。
- 位于根节点的事务称为顶层事务,其他事务称为子事务。事务的前驱称(predecessor.父事务(parent),事务的下一层称为儿子事务(child)
- 子事务既可以提交也可以回滚。但是它的提交操作并不马上生效,除非其父事务已经提交。因此可以推论出,任何子事物都在顶层事务提交后才真正的提交。
- 树中的任意一个事务的回滚会引起它的所有子事务一同回滚,故子事务仅保留A、C、I特性,不具有D的特性。
在Moss的理论中,实际的工作是交由叶子节点来完成的,即只有叶子节点的事务才能访问数据库、发送消息、获取其他类型的资源。而高层的事务仅负责逻辑控制,决定何时调用相关的子事务。即使一个系统不支持嵌套事务,用户也可以通过保存点技术来模拟嵌套事务,如下图所示。
从图7-5中也可以发现,在恢复时采用保存点技术比嵌套查询有更大的灵活性。例如在完成Tk3这事务时,可以回滚到保存点S2的状态。而在嵌套查询的层次结构中,这是不被允许的。
但是用保存点技术来模拟嵌套事务在锁的持有方面还是与嵌套查询有些区别。当通过保存点技术来模拟嵌套事务时,用户无法选择哪些锁需要被子事务继承,哪些需要被父事务保留。这就是说,无论有多少个保存点,所有被锁住的对象都可以被得到和访问。而在嵌套查询中,不同的子事务在数据库对象上持有的锁是不同的。例如有一个父事务P1,其持有对象X和Y的排他锁,现在要开始一个调用子事务S1,那么父事务P1可以不传递锁,也可以传递所有的锁,也可以只传递一个排他锁。如果子事务S1中还要持有对象Z的排他锁,那么通过反向继承( counter-inherited),父事务P1将持有3个对象X、Y、Z的排他锁。如果这时又再次调用了一个子事务S2,那么它可以选择传递那里已经持有的锁。
然而,如果系统支持在嵌套事务中并行地执行各个子事务,在这种情况下,采用保存点的扁平事务来模拟嵌套事务就不切实际了。这从另一个方面反映出,想要实现事务间的并行性,需要真正支持的嵌套事务。
分布式事务(Distributed Transactions) 通常是一个在分布式环境下运行的扁平事务,因此需要根据数据所在位置访问网络中的不同节点。
假设一个用户在ATM机进行银行的转账操作,例如持卡人从招商银行的储蓄卡转账10000元到工商银行的储蓄卡。在这种情况下,可以将ATM机视为节点A,招商银行的后台数据库视为节点B,工商银行的后台数据库视为C,这个转账的操作可分解为以下的步骤:
- 节点A发出转账命令
- 节点B执行储蓄卡中的余额值减去10000。
- 节点C执行储蓄卡中的余额值加上10000
- 节点A通知用户操作完成或者节点A通知用户操作失败。
这里需要使用分布式事务,因为节点A不能通过调用一台数据库就完成任务。其需要访问网络中两个节点的数据库,而在每个节点的数据库执行的事务操作又都是扁平的。对于分布式事务,其同样需要满足ACID特性,要么都发生,要么都失效。对于上述的例子,如果2)、3)步中任何一个操作失败,都会导致整个分布式事务回滚。若非这样,结果会非常可怕。
对于InnoDB存储引擎来说,其支持扁平事务、带有保存点的事务、链事务、分布式事务。对于嵌套事务,其并不原生支持,因此,对有并行事务需求的用户来说MySQL数据库或InnoDB存储引擎就显得无能为力了。然而用户仍可以通过带有保存点的事务来模拟串行的嵌套事务。