事务处理基本概念
事务处理
1.1 基本概念
事务,transaction,是构成单一逻辑工作单元的操作合集。
事务通常由高级数据操纵语言,比如SQL或者编程语言(比如C++)通过JDBC或者ODBC嵌入式数据库访问程序进行执行。
事务基本内容由类似语句beign transaction
和end transaction
语句来规定,两者之间的操作集合作为必须作为一个单一、不可分割的单元出现,属于这个合集中的操作要么都执行,要么都不执行,这种特性称为原子性(atomicity)。
并且数据库必须采取特殊处理来保证事务的正常执行而不被并发执行的数据库语句干扰,这称为隔离性(isolation)。、
当数据库系统崩溃后,事务的操作也必须是持久的,这称为持久性(durability)。
除此之外,事务必须保持数据库的一致性(consistency),即如果一个事务作为原子从一个一致的数据库状态开始独立运行,则事务结束时数据库也必须是再次一致的。
以上性质称为ACID特性,缩写来自4条性质的第一个字母。
1.2 一个简单的事务模型例子
现假设事务使用如下两个操作来访问数据:
read(x)
,从数据库把数据项x
读取到执行该操作事务的主存缓冲区内的一个变量x
中。write(x)
,从执行该操作事务的主存缓冲区的变量x
中把数据项x
写入数据库中。
事务例子\(T_i\)如下:
从上面的例子上看,如果考虑ACID特性:
- 一致性,一致性要求该事务的执行不改变AB之和;
- 原子性,假设事务执行之前A和B的值分别为1000和2000,那么如果故障发生在
write(A)
之后write(B)
之前,此时系统中A和B的值就是950和2000,造成了这种不一致状态;- 不一致状态并非一定是故障产生的,即使正常执行的情况下,系统也会在某一个时刻处于不一致状态,但是原子性则要求这种不一致状态只能在执行当中存在,其他时刻不存在;
- 原子性要求,事务中的所有动作要么全部执行,要么都不执行。
- 保证原子性思路:对于事务要执行写操作的数据项,数据库系统会在磁盘上记录其旧值,该信息记录在日志中;如果事务执行失败,数据库系统就会从日志中恢复其旧值,将系统还原成事务执行之前的状态。保证原子性是数据库本身的责任,由恢复系统(recovery systeem)处理。
- 持久性,当事务执行成功后,系统就需要保证该事务对数据库所做的所有更新都是成就的,即使在事务执行之后系统出现故障。
- 保证持久性:同样由恢复系统处理,且基于一个前提,内存中的数据会在故障后丢失,但是磁盘中的数据库不会丢失。所以只需要确保以下两个条件的任一条件即可:
- 事务执行后的更新在事务结束之前已经写入磁盘。
- 事务的已经执行的更新信息也已经写入磁盘上,且该信息可以帮助系统故障后恢复。
- 保证持久性:同样由恢复系统处理,且基于一个前提,内存中的数据会在故障后丢失,但是磁盘中的数据库不会丢失。所以只需要确保以下两个条件的任一条件即可:
- 隔离性,当几个事务并发执行,即使每个事务都保证了原子性和一致性,但是事务的交叉执行仍然会导致不一致的状态。
- 事务串行执行可以避免产生问题,但是效率太低;
- 数据库系统中的并发控制系统(concurrency-control system)可以确保隔离性,具体来说,确保事务并发执行后的系统状态与这些事务以某种次序串行执行的状态等价。
1.3 存储结构
- 易失性存储,volatile storage,比如内存,其中的数据在系统崩溃后丢失;
- 非易失性存储,nonvolatile storage,比如磁盘, 数据在系统崩溃后不会丢失;
- 稳定性存储,stable storage,数据“永远”不会丢失,通过某些手段降低了故障可能性。
1.4 事务的原子性和持久性
当事务执行失败时,就需要中止事务,且为了保证原子性,中止事务必须对数据库的状态不造成影响,即对数据库所做的所有改变都必须撤销。
成功完成执行的事务称为已提交,而一旦事务已提交,就不能通过中止事务来撤销其造成的影响。
撤销已提交事务造成的影响的唯一方法是执行一个补偿事务(compensating transaction),这个责任被交给了用户而不是数据库系统来处理。
事务的状态转换图如上所示,事务从活动状态开始,当事务执行完它的最后一条语句之后就进入部分提交状态。
此时,事务已经完成了语句的执行,但是实际输出可能还没有写入磁盘,如果此刻发生故障,事务就可能中止。
如果顺利,那么数据库系统就会往磁盘上写入相关信息,以确保系统可以在故障重启后重新创建事务的更新,之后事务就进入提交状态。
当一个事务进入部分提交状态后,只有在没有其他并发事务已经修改该事务想要更新的数据项的情况下,事务才会进入提交状态,而不能提交的事务则中止。
当系统判断事不能正常执行后,事务就会进入失败状态,此时事务就必须回滚。接着事务就会进入中止状态,此时系统有两种选择:
- 重启事务,仅当硬件故障导致的事务中止时使用,其重启的事务会被看作是一个新的事务;
- 杀死事务,当中止原因是事务内部逻辑错误时;
同时需要小心处理可见的外部写(observable external write)动作,比如写入用户屏幕时,一般的处理方式是在磁盘上临时写入相关数据,在事务进入提交状态之后再进入外部写操作。
1.5 事务的调度 & 可串行化
在并发执行中,保证并发执行的效果和没有并发时相同,就可以确保数据库的一致性,即调度在某种程度上等价于一个串行调度,这种调度称为可串行化。
并发控制的前提是,如何保证一个调度是可串行化的?关键在于指令之间的冲突问题。
对于AB两个不同事务在同一个数据项上的操作,当其中有一个写指令时,两个指令就是冲突的,因为执行次序和结果强相关。
这里有几个概念:
- 冲突等价,conflict equivalent,当调度S可以经过一系列非冲突指令交换得到S’,那么两个调度是冲突等价的;
- 冲突可串行化,conflict serializable,如果调度S和一个串行调度冲突等价,那么调度S就是冲突可串行化的。
而串行化顺序可以通过拓扑排序得到,要判定冲突可串行化,就需要构造优先图,并检测是否存在环,如果存在则说明该调度不是冲突可串行化的。
1.6 事务的隔离性和原子性
在并发执行过程中,事务失败的影响必须被撤销才能保证其原子性。而从事务故障恢复的角度来看以下两种调度才是可以接受的调度:可恢复调度和无级联调度。
可恢复调度,对于每对事务\(T_i\)和\(T_j\),如果\(T_j\)读取了\(T_i\)之前写的数据项,那么\(T_i\)必须先于\(T_j\)提交。依赖事务在被依赖事务之前提交。
一个不可恢复调度的例子如下:
事务\(T_7\)在\(read(A)\)之后立刻提交了,但是此时\(T_6\)还在活跃状态,\(T_7\)读取了\(T_6\)写入A的值。如果\(T_6\)提交之前发生故障,因为\(T_7\)依赖于\(T_6\)(读取了\(T_6\)之前写入的数据),所以\(T_7\)也必须回滚。
但是因为\(T_7\)已经提交,不能中止\(T_7\)来保证事务的原子性,所以在\(T_6\)发生故障后不能正确恢复。
无级联调度,对于每对事务\(T_i\)和\(T_j\),如果\(T_j\)读取了先前由\(T_i\)所写的数据项,那么\(T_i\)必须在\(T_j\)读操作之前提交,每个无级联调度也是客回复调度,区别在于,无级联调度要求被依赖事务必须在依赖事务的读操作之前提交。
一个级联回滚(cascading rollback)的例子如下,当事务\(T_8\)失败需要回滚时,因为\(T_9\)和\(T_{10}\)都依赖于\(T_8\),一个事务故障导致一系列事务回滚,需要撤销大量工作。
1.7 事务隔离性级别
SQL规定的隔离性级别如下:
- 可串行化
- 可重复读,只允许读取已提交的数据,且在一个事务内两次读取同一个数据项之间,其他事务不能更新该数据。但该事务不要求于其他事务可串行化。
- 已提交读,只允许读取已提交数据,但不要求可重复读。
- 未提交读,允许读取未提交数据,这是SQL允许的最低一致性级别。
以上所有隔离级别都不允许脏写(dirty write),即如果一个数据项已经被另外一个尚未提交或者中止的事务(正常活跃的事务)写入,就不允许对该数据项执行写操作。
大多数数据库默认级别是已提交读,也可以显示设置隔离性级别,而修改级别必须作为事务的第一条语句执行。
1.8 隔离性级别的实现
在数据库系统中,通过多种并发控制机制(concurrency-control scheme)来保证多个事务鬓发执行时,只产生可接受的调度。比如锁、时间戳和快照隔离。
1.8.1 锁
一个事务可以封锁其访问的数据项,而不用封锁整个数据库。通过持有锁来保证串行化,但是持有周期不能过长否则会影响性能。
锁一般有两种,共享锁用于读操作,排他锁用于写操作。多个事务可以同时持有同一个数据项上的共享锁,但只有当其他事务在一个数据项上不持有任何锁时,一个事务才允许持有该数据项的排他锁。
以上两种锁模式以及两阶段协议在保证可串行化的前提下允许数据的并发读。
1.8.2 时间戳
对于每个数据项,系统维护两个时间戳,读时间戳记录读取该数据项的事务的最近时间戳;写时间戳记录写入该数据项的当前值的事务的时间戳;
当访问冲突的时候,事务会按照时间戳的顺序来访问数据项,当无法访问时,违例事务会中止,然后分配一个新的时间戳开始。
1.8.3 多版本和快照隔离
通过维护数据项的多个版本,一个事务就可以读取一个旧版本的数据项,而不是被另一个未提交或者在串行化序列中排在后面的事务写入的新版本数据项。
多版本维护,广泛使用的是快照隔离(snapshot isolation)技术。
快照隔离中,每个事务开始时有自身数据库版本或者快照,它从这个版本中读取数据,来和其他事务的更新隔离开。
即当事务更新数据库时,更新只会出现在其私有版本而不是数据库本身,直到事务提交时,更新才会被写入数据库。
快照隔离可以保证读数据的尝试无需等待,因为读取的是它自己的快照,且大部分事务都是只读的,所以和锁相比会带来性能改善。
但是快照隔离下,任何事务都不能看到对方的更新,这可能会导致不一致状态出现。