<oracle-7> 事务

事务(transaction)是数据库区别于文件系统的特性之一。在文件系统中,如果你正把文件写到一半,操作系统就崩溃了,这个文件就很可能会被破坏。这正是数据库中引入事务的主要目的, 事务会把数据库从一种一致状态转变为另一种一致状态。在数据库中提交工作时,可以确保要么所有修改都已经保存,要么所有修改都不保存。

Oracle中的事务体现了所有必要的ACID特征。
原子性(Atomicity):事务中的所有动作要么都发生,要么都不发生。
一致性(Consistency):事务将数据库从一种一致状态变为下一种一致状态。
隔离性(Isolation):一个事务的影响在该事务提交前对其他事务都不可见。
持久性(Durability):事务一旦提交,其结果就是永久性的。

7.1 事务控制语句
Oracle中不需要专门的语句来“开始事务”。事务会在修改数据的第一条语句隐式开始(也就是得到TX锁的第一条语句)。也可以使用set transaction或DBMS_TRANSACTION包来显式地开始一个事务,但是这一步并不是必要的, 这与其他的许多数据库不同,因为那些数据库中都必须显式开始事务。如果发出commit或rollback语句(注意rollback to savepoint不会结束事务),就会显式地结束一个事务。

如果正常退出sql*plus会话,而没有提交或回滚事务, sql*plus会自动完成提交。不要过分依赖这些隐式行为,一定要显式提交或回滚。
Oracle中的事务是原子性的,这说明无非两种情况:构成事务的每条语句都会提交(成为永久)或者所有语句都回滚。注意这里的“所有”,也就是当成了一个整体。

我们可以了解到控制事务的语句只有:commit,rollback, savepoint, rollback to <savepoint>,set transaction五种。

7.2 原子性
前面对事务控制语句做了一个简要的概述后,下面可以看看语句原子性、过程原子性和事务原子性的含义。

7.2.1 语句级原子性
考虑以下语句:
insert into t values(1);
如果该语句由于一个约束冲突而失败,这一行就不会插入。不过再考虑下面的例子,表T上的一个insert或delete会触发一个触发器,它将适当地调整t2表上的列:
T2: create table t2 (cnt int);
T: create table t (x int check (x > 0));

创建触发器:
create trigger t_trigger
    before insert or delete on t for each row
    begin
        if (inserting) then
            Update t2 set cnt = cnt + 1;
        else
            update t2 set cnt = cnt – 1;
        end if;
       dbms_output.put_line(‘I fired and updated’ || sql%rowcount || ‘ rows’);
    end;
/

这种情况下往T表插入数据,会发生什么就不那么显而易见了。如果触发器触发之后出现了错误,触发器的影响是否还存在?也就是如果触发器被触发,并且更新了t2,但是这一行没有插入到T中,结果会怎样?期望的结果是,如果并没有真正在T中插入一行,我们就不希望T2中的cnt列增加。幸运的是,在oracle中,客户最初发出的语句(在这里就是insert into t)会完全成功或完全失败。这个语句是原子性的,同时insert into T的任何连带效果都被认为是该语句的一部分,这里就是触发器。

为了得到这种语句级原子性,oracle悄悄地在每个数据库调用外面包了一个savepoint。前面的insert 语句实际处理如下:
savepoint statement1;
    Insert into t values (1);
If error then rollback to statement1;
savepoint statement2;
    Insert into t values (-1);
if error then rollback to statement2;

Sybase或sqlserver中刚好相反。这些系统中的触发器会独立于触发语句执行。如果触发器遇到一个错误,它 必须显式地回滚自己的工作,然后产生另外一个错误来回滚触发语句。

7.2.2 过程级原子性
有意思的是,oracle把pl/sql匿名块也当做是语句。比如以下存储过程:
Create or replace procedure p as
    Begin
        Insert into t values (1);
        Insert into t values (-1);
    End;
/

继续沿用上节的check约束,在这个过程中,第二个insert总会失败。这时候我们调用这个存储过程会发现表t里总是空的。也就是整体没有插入数据。但是如果我们调用p的时候自己捕获了错误,那第一句插入就会成功。因为oracle没有捕获到异常发生,当做事务成功,就会提交。

7.2.3 事务级原子性
事务的总目标是把数据库从一种一致状态转变为另一种一致状态。为了实现这个目标,事务也是原子性的,事务完成的所有工作要么完全提交称为永久性的,要么回滚并撤销。

7.2.4 DDL与原子性
需要指出,oracle中有一类语句具有原子性,不过只是在语句级保证原子性。DDL(数据定义语言, Data Definition Language, DDL)语句采用了一种特定的实现方式,可以完成如下工作:
(1) 提交当前所有未完成的工作,结束当前已有的所有事务。
(2) 完成DDL操作,如create table.
(3) 如果DDL操作成功则提交,否则回滚DDL操作。

7.3 持久性
通常情况下,一个事务提交时,它的改变就是永久性的。即使数据库在提交完成之后随即崩溃,你也完全可以相信这些改变确实已经永久存储在数据库中。不过,下述两种情况例外:
(1) 使用commit语句新增的write扩展(这是oracle database 10g release2及以上版本中新增的特性)。
(2) 在非分布式(只访问一个数据库,而不是多个数据库连接)pl/sql代码块中执行commit.

7.3.1 commit的write扩展
在oracle 10g release2及以上版本中,可以为commit语句增加一个write子句。这个write子句允许等到生成的redo写至磁盘之后再提交(wait,这是默认选项),或者不等写redo就直接提交(nowait)。
通常情况下,commit是一个同步过程,应用首先调用commit,然后等待整个commit处理完成。在oracle 10g release2之前,所有oracle版本支持的都是这种commit行为,这也是oracle 10g release2及以上版本的默认行为。
Oracle的当前版本中,并不需要等待提交完成(这需要一定的时间,因为提交涉及一个物理IO操作,要向存储在磁盘上的redo日志文件完成物理写操作),可以在后台完成提交。这会带来一个副作用:提交不能保证持久性。也就是说,应用可能会从该数据库得到一个响应,指出已经收到你的异步提交,而且其他会话能够看到你做出的改变,不过后来却发现你原以为已经提交的事务实际上并未真正提交。这种情况很少见,往往涉及严重的硬件或软件故障。要让一个异步提交不具有持久性,数据库必须异常关闭,这意味着数据库实例或运行这个数据库实例的计算机必须完全失效。
那么,既然事务要保证持久性,这种可能导致非持久性的特性又有什么用呢?答案在于性能。在应用中发出一个commit时,会请求LGWR进程得到生成的redo,并确保将生成的这些redo写至在线redo日志文件。完成物理IO操作的速度相当慢,所以,commit甚至比事务中DML语句本身耗时还要长。如果异步完成commit,就不再需要等待客户应用中的物理IO,这会让客户应用速度更快,特别是如果有大量commit,异步提交会让速度大大提高。
既然如此,为什么不总是使用commit write nowait呢?记住, 正确性永远位于首位。只有3种特殊情况才需要使用nowait:
(1) 定制的数据加载程序。
(2) 处理某种实时数据,这些数据对时间敏感,即使失败也可能会被覆盖或丢弃。
(3) 应用实现了自己的“排队”机制。
你会注意到这3种应用个都是后台的非交互式应用,他们不会与人直接交互。如果应用需要与人交互,向用户报告“提交完成”,就应当使用同步提交。对于面向客户的在线应用,不能把异步提交作为改善性能的手段。异步提交只适应于面向批处理的应用。因此除了这3类批处理应用外,其他应用中都不该使用这个新功能—commit write nowait。

7.3.2 非分布式pl/sql代码块中的commit
从oracle6引入pl/sql以来,pl/sql一直都透明地使用异步提交。这种做法是可行的,因为从某种意义来说所有pl/sql都类似于批处理程序,即在pl/sql过程完全执行完之前,最终用户无法知道过程的结果。也正是因为这个原因,这种异步提交只能用于非分布式的pl/sql代码块;如果涉及到多个数据库,会有两个对象(两个数据库)依赖于提交的持久性。倘若两个数据库都依赖于提交的持久性,就必须采用同步协议,否则可能在一个数据库中改变已经提交,而在另一个数据库中未提交。

7.4 完整性约束和事务
默认情况下,完整性约束会在整个sql语句得到处理之后才进行检查。也有一些可延迟的约束允许将完整性约束的验证延迟到应用请求时(发出一个set constraints all immediate命令)才完成,或者延迟到发出commit时再检查。

7.4.1 immediate约束
在讨论的前一部分,我们假设约束都是immediate模式,这也是一般情况。在这种情况下,完整性约束会在整个sql语句得到处理之后立即检查。注意,这里用的是“sql语句”而不是“语句”。如果一个pl/sql存储过程中有多条sql语句,那么在每条sql语句执行之后都会立即验证其完整性约束,而不是在这个存储过程完成后才检查它。比如update语句会影响多条语句,则会在所有需要更新的语句更新完后才检查一次,而不是每变化一行数据就检查一次。

7.4.2 deferrable约束和级联更新
从oracle8开始,我们还能够延迟约束检查,对于许多操作来说,这很有好处。首先能想到的是可能需要将一个主键的update级联到子键。在以前的版本中,确实也可以完成cascade udpate,但是为此需要做大量的工作,而且存在某些限制。有了可延迟约束后,这就变得易如反掌了。

7.5 不好的事务习惯
许多开发人员在事务方面都有一些不好的习惯。例如 在Informix、sybase和sqlserver中,必须显式地begin一个事务否则,每条单个的语句本身就是一个事务。Oracle在具体的语句外包了一个savepoint,采用类似的方式,哪些数据库则在各条语句外报了一个begin work/commit或rollback。这是因为,在这些数据库中,锁是稀有资源,另外读取器会阻塞写入器,反之,写入器也会阻塞读取器。为了提高并发性,这些数据库希望你的事务越小越好,有时甚至会以数据完整性为代价来做到这一点。
Oracle则采用了完全不同的方法。事务总是隐式的,没有办法“自动提交”事务,除非应用专门实现。在oracle中,每个事务都应该只在必要时才提交。事务的大小要根据需要而定。锁、阻塞等问题并不是决定事务大小的关键,数据完整性才是确定事务大小的根本。

7.5.1 在循环中提交
如果交给你一个任务,要求更新多行,大多数程序员都会力图找出一种过程性方法,通过循环来完成这个任务,这样就能提交多行。通常这样做的两个主要原因是:
(1) 频繁地提交大量小失误比处理和提交一个大事务快;
(2) 没有足够的undo空间。
这两个原因都存在误导性。另外,如果提交地太过频繁,很容易陷入危险,倘若更新做到一半时失败了,就会使你的数据库处于一种未知的状态。到目前为止,最好的办法是按业务过程的要求以适当的频度提交,并且相应地设置undo段的大小。

7.5.2 使用自动提交
关于不好的事务习惯,最后要说的是由于使用流行的编程api(odbc或jdbc)所带来的问题。这些api会默认“自动提交”(autocommit)。使用jdbc的一个好习惯是获取连接后,设置 setAutocommit(false);
但是设置之后,使用更加要小心,不仅要手动commit,在失败的地方还要手动rollback,否则如果是使用了连接池,那就不会释放锁,导致锁表。还可以参考:
http://ygsilence.iteye.com/blog/1297762

7.6 分布式事务
Oracle有很多很好的特性,其中之一就是能够透明的处理分布式事务。在一个事务的范围内,可以更新多个不同数据库的数据。提交时,要么提交所有实例的更新,要么一个都不提交(它们都会回滚)。为此,我们不需要另外编写任何代码,只是提交就行了。

Oracle中分布式事务的关键是 数据库链接(database link)。数据库链接是一个数据库对象,描述了如果从你的实例登陆到另一个实例。一旦建立了一个数据库链接,访问远程对象就很简单了,如下:
Select * from T@another_database;

这会从数据库链接another_database所定义数据库实例的表T中选择。一般地,你会创建表T的一个视图(或一个同义词)来隐藏T是一个远程表的事实。比如:create synonym T for T@another_database;
现在执行一个分布式事务与执行一个本地事务没什么两样:
Update local_table set x=5;
Update remote_table@another_database set y=10;
Commit;
Oracle会完成所有数据库中的提交,或都不提交。它 使用了一个2PC(two-phase commit protocol,二段提交协议)来做到这一点。2PC是一个分布式协议,如果一个修改影响到多个不同的数据库,2PC允许原子性地提交这个修改。

对于分布式事务中能做的事情,还存在一些限制,这些限制是合理的:
(1) 不能在数据库链接上发出commit。也就是说,不能发出commit@remote_site,只能从发起事务的那个站点提交。
(2) 不能在数据库链接上完成DDL,因为DDL会提交,违反上面那条限制。
(3) 不能在数据库连接上发出savepoint.

猜你喜欢

转载自zoroeye.iteye.com/blog/2179630