事务中的基本概念
在数据库中,会将有意义的数据操作进行集中处理,利用数据库进行数据的检索、插入、更新、删除操作, 这里用户的一个连贯的操作,就是事务(transaction)。
为了解决多个用户同时访问数据库时不会发生问题,就需要控制这些操作, 也就是通常意义上的并发控制中的上锁(LOCK)操作(乐观并发不上锁的情况不在本文讨论范围内)。
上锁的意义在于防止对重要数据的误操作。 比如两笔对同一账户的转账,如果并发操作,可能第二次转账未读取到第一次转账后的结果时, 会造成账户的金额错误。这种情况下,第二次操作不能在第一次操作结束之前进行操作。
当各种事务操作正确完成时,确认对数据库的处理过程就是提交(COMMIT)。 同时,由于数据库是多人共享数据,非常频繁的上锁会导致性能问题, 一般情况下需要根据情况使用锁。
• 用户仅进行读操作时,使用共享锁,在上共享锁时,其他用户能够读取数据,但不能写入。
• 用户写操作时,使用独占锁,上独占锁时,其他用户既不能读取又不能写入。
使用锁控制多个事务执行就被称之为并发控制(concurreny control)。 在使用并发控制后,即使再多的人使用数据库也不会发生矛盾。
这里假设有事务A对于账户I上了独占锁,同时事务B对账户J上了独占锁,如果这时A想给J上独占锁, B想给I上独占锁时,由于两个事务都等待对方解除锁,处理就进行不下去了,这时就产生了死锁(deadlock), 数据库中一般都会有预防死锁的机制的,有的数据库采用超时机制,持续一段时间以后, 事务会采取取消的方式,取消事务就是数据库中的回滚(rollback)。为了防止出现安全性问题, 数据库中事务包含的一系列操作要么全部成功要么全部失败,不允许出现中间状态。 而为了防止发生故障,而导致事务的中断产生问题,就需要引入事务日志,在故障恢复后, 读取事务日志,对数据库进行恢复。
事务的性质
由于数据库的用户众多,并且多个数据库操作同时执行,同时可能发生故障,不允许各事务之间发生影响。 因此,数据库事务应该满足一些特性,这些特性就是通常称之为ACID的特性。
ACID特性如下表所示:
特性
内容
含义
A-Atomicity
原子性
数据库事务必须结束在提交或回滚
C-Consistency
一致性
执行数据库事务不能损坏数据库的一致性
I-Isolation
隔离性
两个事务执行不能相互干扰,一个事务不能看到其他事务运行时的中间某一时刻的数据
D-Durability
持久性
在事务完成之后,事务对数据库所做的更改要持久化,将数据保存在数据库中,不能被回滚
- 特性1,原子性
首先,数据库事务要具备原子性。数据库事务需要提交或者回滚来结束。 提交是确定数据库事务处理的指令,回滚是取消数据库事务处理的指令。 提交或者回滚可以自动执行,或者显式执行。显式执行中,根据是否发生错误, 变更数据库事务处理。一般在数据库中显式指令为提交COMMIT和回滚ROLLBACK。 满足原子性就要求事务的操作序列或者完全应用于数据库或者完全不影响数据库。 如果用户完成了提交,这时所有的更新对于外部世界必须完全可见,如果用户完成了 回滚,要求完全不可见或者说完全没有影响。 - 特性2,一致性
数据库事务要具备一致性。数据库执行前后数据库不存在矛盾。例如, 数据库账户表某账户有1万,A加1万,B加1万,结果不能变成2万,避免出现更新遗失 (lost update)。并行处理数据库的事务,多个事务可能同时访问相同的表或行。 此时,根据事务的处理顺序不能发生矛盾。即事务对于数据库中相同的资源的访问, 不会发生矛盾。并发控制手段有锁(2PL、3PL),时间戳控制(TO),乐观控制(OCC)等。 - 特性3,隔离性
在数据库中执行的事务不断增加,能够逐渐控制事务之间相互干涉的级别就叫做隔离级别 (isolation level)。隔离级别分为下列四个级别:
– READ UNCOMMITTED:允许读取未提交数据。SQL允许的最低一致性级别。
– READ COMMITTED:只允许读取已提交,但不要求可重复读。
– REPEATABLE READ:只允许读取已提交数据,并且在一个事务两次读取一个数据项期间, 其他事务不得更新该数据。
– SERIALIZABLE:保证可串行化调度。
通过设置隔离级别,可能出现三种现象。
– 脏读(dirty read):指事务1在提交前事务2读取到了共同访问的行, 在事务1回滚的情况下,事务2读取到了不存在的行。
– 非重复读(non-repeatable read):事务1读取行时,事务2更新这行并提交时, 事务1再次读取这行,数值发生不一致的现象。
– 幻读(phantom):指事务1进行检索,获得多行结果,事务2追加了符合该条件的行, 事务1第二次检索的结果发生不同的现象。
各隔离级别发生现象的表:
特性
脏读
非重复读
幻读
读取未提交
可能发生
可能发生
可能发生
读取已提交
不可能
可能发生
可能发生
可重复读
不可能
不可能
可能发生
可序列化
不可能
不可能
不可能
可见事务级别越高,事务越安全,距离可序列化保证越近,但事务级别越高, 性能是在下降的,所以一般情况下,传统关系型数据库默认的事务级别都不是可序列化级别, 可能是读取已提交,或者类似于这个级别的自定义级别-快照隔离。
事务的故障恢复概念
在数据库设计时需要将数据库故障视为常态,系统发生的故障有很多种。 每种故障都需要不同的方法来处理,而事务故障就是在设计事务管理器中首先需要考虑的事情。
一般来说,事务故障可能发生两种:
• 逻辑错误:事务由于某些内部条件而无法继续正常执行,这些内部条件包括非法输入、 找不到数据、溢出或超出资源限制。
• 系统错误:系统进入一种不良状态(如死锁),结果事务无法继续正常执行。
考虑事务原子性,如通过在事务执行过程中发生了系统崩溃,如果执行中有内存的内容, 在崩溃时,无法知道其中事务的内容,系统重启时就会出现不满足事务原子性的情况, 可能事务已经完成,可能事务完成一半,可能事务完全没成功,根本无法确认。
原子性的目标是要么执行了事务对数据库的所有修改,要么都不执行。但是如果事务 执行多处数据库修改,可能需要多个输出操作,并且故障可能发生于某些修改完成后 全部修改完成前。
为了达到保持原子性的目标,必须在修改数据库本身之前,首先向磁盘输出信息, 描述要做的修改。这些信息要确保已经提交事务所做的所有修改都能反映到数据库, 或者在故障后的恢复过程中反映到数据库中。这种信息还有一个好处,就是确保在 中止事务时事务所做的任何修改都不会持久存在于数据库中。
这里应用最广泛的记录数据库修改的结构数据就是日志。日志记录数据库中所有更新活动。
传统数据库中的事务日志方案,日志记录有几种。
采用更新日志记录描述一次数据库写操作,一般有下列字段:
• 事务日志,执行写操作的事务的唯一标识。
• 数据项标识,是所写数据项的唯一标识。通常是数据项在磁盘上的位置, 包括数据项所驻留的块的快标识和块内偏移量。
• 旧值,数据项的写前值
• 新值,数据项的写后值
例如将更新日志记录记为<Ti,Xi,Vo,Vn>
除了更新日志记录外,其他的日志记录用于记录事务处理过程中的重要事件, 如事务的开始以及事务的提交或中止。例如:
• 事务开始记为<Ti,start>
• 事务提交记为<Ti,commit>
• 事务中止记为<Ti,abort>
每次事务执行写操作时,必须在数据库修改前建立该次写操作的日志记录,并将其加入到日志中。 一个日志记录一旦存在,就可以根据需要将修改输出到数据库中。并且可以根据日志, 撤销已经输出到数据库中的修改,也就是利用了更新日志记录中的写前值。
一般将日志记录存放在稳定存储器中,以便在系统故障或磁盘故障时恢复。
数据库中的undo和redo,Redo与Undo并非是相互的逆操作,而是能配合起来使用的两种机制。 Redo用来保证事务的原子性和持久性,Undo能保证事务的一致性,两者也是系统恢复的基础前提。
• undo使用一个日志记录,将该日志记录中指明的数据项设置为旧值。
在进行级联回滚时可将日志中的读数据项看做UNDO型日志记录。 如果事务在更新数据库之后但在提交之前,无论由于什么原因引发故障, 都可能需要对事务进行回滚。如果事务已修改了任何数据项值并写入数据库, 必须将它们恢复为原值,使用UNDO型日志记录将必须回滚的数据项恢复为其旧值。
• redo使用一个日志记录,将该日志记录中指明的数据项设置为新值。
redo是在恢复时将所有数据项都更新为新值,所以对于通过redo来执行更新的执行顺序很重要, 如果崩溃恢复时,数据项的更新执行顺序不同于原来它们的执行顺序,那么该数据项的 最终状态将是一个错误值。大多数的恢复算法,都没有把每个事务的重做分别执行, 而是对日志进行一次扫描,在扫描过程中,每遇到一个redo日志记录,就执行一次redo动作。这种方法能确保保持更新的顺序,并且效率更高,因为仅需要整体读一遍日志, 而不是对每个事务读一遍日志。
还有一种日志记录类型是检查点(check point),当系统发生故障时,必须检查日志, 决定哪些事务需要undo和redo,原则上,需要搜索整个日志来确定消息,但存在两个问题:
- 搜索耗时过大。
- 大多说需要重做的事务已将更新写入数据库了,尽管进行redo不会存在问题, 但会使恢复过程变得过长。
为了降低这种开销,需要引入检查点。系统周期性地把所有被修改的数据库缓存写入磁盘时, 相应地在日志中写入<checkpoint,L>记录(L检查点时活跃的事务列表)。当系统发生崩溃时, 对于日志中记录检查点之前所有具有提交记录的事务,无需对这些事务的写操作进行redo, 因为在检查点之前,它们的所有更新都已经保存到了磁盘的数据库中。
检查点的执行过程: - 将当前位于主存的所有日志记录输出到存储。
- 将所有修改的缓存块输出到存储。
- 将日志记录<checkpoint,L>输出到存储。
检查点引入时: - 在执行检查点操作时不允许执行任何更新
- 在执行检查点过程中将所有更新过的缓存都输出到磁盘。
由于存在以上两个限制条件,也会引起相当大的麻烦,因为这时,事务处理会出现停顿。 所以就出现了模糊检查点(fuzzy checkpoint),它允许在缓存块正在写入时也允许事务执行更新。
以上为事务基础及特性,「分布式技术专题」是由hubble数据库团队精心整编,专题会持续更新,欢迎大家保持关注。




