1. 事务ACID特性
原子性(Actomicity):事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。
一致性(Consistent):一致性指事务将数据库从一种状态转变为下一种一致的状态。在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。例如,在表中有一个字段为姓名,为唯一约束,即在表中姓名不能重复。如果一个事务对姓名字段进行了修改,但是在事务提交或事务操作发生回滚后,表中的姓名变得非唯一了,这就破坏了事务的一致性要求,即事务将数据库从一种状态变为了一种不一致的状态。因此,事务是一致性的单位,如果事务中某个动作失败了,系统可以自动撤销事务——返回初始化的状态
隔离性(Isolation):数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然。
-
持久性(Durable):事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。
2. 事务分类
从事务理论的角度来说,可以把事务分为以下几种类型:
❑扁平事务(Flat Transactions)
❑带有保存点的扁平事务(Flat Transactions with Savepoints)
❑链事务(Chained Transactions)
❑嵌套事务(Nested Transactions)
❑分布式事务(Distributed Transactions)
2.1 扁平事务
扁平事务(Flat Transaction)是事务类型中最简单的一种。在扁平事务中,所有操作都处于同一层次,其由BEGIN WORK开始,由COMMIT WORK或ROLLBACK WORK结束,其间的操作是原子的,要么都执行,要么都回滚。因此扁平事务是应用程序成为原子操作的基本组成模块。
但是使用扁平事务也有缺点。例如用户在旅行网站上进行自己的旅行度假计划。用户设想从杭州到意大利的佛罗伦萨,这两个城市之间没有直达的班机,需要用户预订并转乘航班,或者需要搭火车等待。用户预订旅行度假的事务为:
BEGIN WORK
S1:预订杭州到上海的高铁
S2:上海浦东国际机场坐飞机,预订去米兰的航班
S3:在米兰转火车前往佛罗伦萨,预订去佛罗伦萨的火车
但是当用户执行到S3时,发现由于飞机到达米兰的时间太晚,已经没有当天的火车。这时用户希望在米兰当地住一晚,第二天出发去佛罗伦萨。这时如果事务为扁平事务,则需要回滚之前S1、S2、S3的三个操作,这个代价就显得有点大。因为当再次进行该事务时,S1、S2的执行计划是不变的。也就是说,如果支持有计划的回滚操作,那么就不需要终止整个事务。因此就出现了带有保存点的扁平事务。
2.2 带有保存点的扁平事务
带有保存点的扁平事务(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。
2.3 链事务
链事务(Chained Transaction)可视为保存点模式的一种变种。带有保存点的扁平事务,当发生系统崩溃时,所有的保存点都将消失,因为其保存点是易失的(volatile),而非持久的(persistent)。这意味着当进行恢复时,事务需要从开始处重新执行,而不能从最近的一个保存点继续执行。
链事务的思想是:在提交一个事务时,释放不需要的数据对象,将必要的处理上下文隐式地传给下一个要开始的事务。注意,提交事务操作和开始下一个事务操作将合并为一个原子操作。这意味着下一个事务将看到上一个事务的结果,就好像在一个事务中进行的一样。下图显示了链事务的工作方式:
链事务与带有保存点的扁平事务不同的是,带有保存点的扁平事务能回滚到任意正确的保存点。而链事务中的回滚仅限于当前事务,即只能恢复到最近一个的保存点。对于锁的处理,两者也不相同。链事务在执行COMMIT后即释放了当前事务所持有的锁,而带有保存点的扁平事务不影响迄今为止所持有的锁。
2.4 嵌套事务
嵌套事务(Nested Transaction)是一个层次结构框架。由一个顶层事务(top-level transaction)控制着各个层次的事务。顶层事务之下嵌套的事务被称为子事务(subtransaction),其控制每一个局部的变换。嵌套事务的层次结构如图所示:
2.5 分布式事务
分布式事务(Distributed Transactions)通常是一个在分布式环境下运行的扁平事务,因此需要根据数据所在位置访问网络中的不同节点。
假设一个用户在ATM机进行银行的转账操作,例如持卡人从招商银行的储蓄卡转账10 000元到工商银行的储蓄卡。在这种情况下,可以将ATM机视为节点A,招商银行的后台数据库视为节点B,工商银行的后台数据库视为C,这个转账的操作可分解为以下的步骤:
1)节点A发出转账命令。
2)节点B执行储蓄卡中的余额值减去10 000。
3)节点C执行储蓄卡中的余额值加上10 000。
4)节点A通知用户操作完成或者节点A通知用户操作失败。
这里需要使用分布式事务,因为节点A不能通过调用一台数据库就完成任务。其需要访问网络中两个节点的数据库,而在每个节点的数据库执行的事务操作又都是扁平的。对于分布式事务,其同样需要满足ACID特性,要么都发生,要么都失效。对于上述的例子,如果2)、3)步中任何一个操作失败,都会导致整个分布式事务回滚。若非这样,结果会非常可怕。
3. 事务的实现
原子性、一致性、持久性通过数据库的redo log和undo log来完成。redo log称为重做日志,用来保证事务的原子性和持久性。undo log用来保证事务的一致性。
有的DBA或许会认为undo是redo的逆过程,其实不然。redo和undo的作用都可以视为是一种恢复操作,redo恢复提交事务修改的页操作,而undo回滚行记录到某个特定版本。因此两者记录的内容不同,redo通常是物理日志,记录的是页的物理修改操作。undo是逻辑日志,根据每行记录进行记录。
4. 事务隔离级别
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
未提交读(Read uncommitted) | √ | √ | √ |
已提交读(Read committed) | x | √ | √ |
可重复读(Repeatable read) | x | x | √ |
可序列化(Serializable) | x | x | x |
关于脏读,不可重复读,幻读这三个问题的描述在前面介绍锁那一篇文章已经讲过了。mysql数据库默认的隔离级别是可重复读。即解决了脏读和不可重复读的问题。但实际上可重复读本身也是有问题的,即查询结果始终为事务开始的时候的状态,即使其他事务已经修改了数据。虽然数据一致性得到了保证,但是逻辑上还是有缺陷。为此在特定场合下还是需要在事务开始时候的查询语句上加上for update。
另外在可重复读隔离级别下,还是存在幻读问题。为解决该问题,需要使用next-key锁(即行锁+间隙锁)。