事 务

 

 

Oracle中的事务体现了所有必要的ACID特征。ACID是以下4个词的缩写:

l         原子性(atomicity) :事务中的所有动作要么都发生,要么都不发生。

l         一致性(consistency) :事务将数据库从一种一致状态转变为下一种一致状态。

l         隔离性(isolation) :一个事务的影响在该事务提交前对其他事务都不可见。

l         持久性(durability) :事务一旦提交,其结果就是永久性的。

 

8.1   事务控制语句

Oracle 中不需要专门的语句来“开始事务”。隐含地,事务会在修改数据的第一条语句处开始(也就是得到 TX 锁的第一条语句)。

可以使用哪些事务控制语句:

COMMIT:要想使用这个语句的最简形式,只需发出COMMIT

ROLLBACK:要想使用这个语句的最简形式,只需发出ROLLBACK

SAVEPOINTSAVEPOINT允许你在事务中创建一个“标记点”(marked point) ,一个事务中可以有多个SAVEPOINT

ROLLBACK TO <SAVEPOINT>:这个语句与SAVEPOINT命令一起使用。可以把事务回滚到标记点,而不回滚在此标记点之前的任何工作。

SET TRANSACTION:这条语句允许你设置不同的事务属性,如事务的隔离级别以及事务是只读的还是可读写的。使用手动undo管理时,还可以使用这个 来指示事务使用某个特定的 undo 段,不过不推荐这种做法。

 

8.2   原子性

8.2.1 语句级原子性

Oracle 保证最初的 INSERT(即导致触发器触发的插入语句)是原子性的,这个INSERT INTOT是语句,所以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;

Oracle中,这种语句级原子性可以根据需要延伸。在前面的例子中,如果INSERT INTO T触发了一个触发器,这个触发器会更新另一个表,而那个表也有一个触发器,它会删除第三个表(以此类推),那么要么所有工作都成功,要么无一成功。为保证这一点,无需你编写任何特殊的代码,Oracle本来就会这么做。

8.2.2 过程级原子性

OraclePL/SQL匿名块也当作是语句。

ops$tkyte@ORA10G> create or replace procedure p

2 as

3 begin

4 insert into t values ( 1 );

5 insert into t values (-1 );

6 end;

Oracle 把这个存储过程调用处理为一个原子语句。客户提交了一个代码块 BEGIN  P;  END;Oracle 在它外面包了一个SAVEPOINT。由于 P 失败了,Oracle 将数据库恢复到调用这个存储过程之前的时间点。下面,如果提交一个稍微不同的代码块,会得到完全不同的结果:

ops$tkyte@ORA10G> begin

2 p;

3 exception

4 when others then null;

5 end;

6 /

在此,我们运行的代码块会忽略所有错误,这两个代码块的输出结果有显著的差别。尽管前面第一个 P 调用没有带来任何改变,但在这里的P调用中,第一个INSERT会成功,而且T2中的CNT列会相应地递增。

Oracle把客户提交的代码块认为是“语句”。这个语句之所以会成功,因为它自行捕获并忽略了错误,所以If error then rollback…没有起作用,而且执行这个语句后Oracle没有回滚到SAVEPOINT。因此,这就保留了P完成的部分工作。为什么会保留这一部分工作呢?首要的原因是 P 中有语句级原子性:P 中的每条语句都具有原子性。P提交其两条 INSERT 语句时就成为 Oracle 的客户。每个INSERT 要么完全成功,要么完全失败。从以下事实就可以证明这一点:可以看到,T上的触发器触发了两次,而且将T2更新了两次,不过T2中的计数只反映了一个UPDATEP中执行的第二个INSERT外包着一个隐式的SAVEPOINT

 

8.2.3 事务级原子性

事务也是原子性的,事务完成的所有工作要么完全提交并成为永久性的,要么会回滚并撤销。

 

8.3   完整性约束和事务

需要指出到底什么时候检查完整性约束。默认情况下,完整性约束会在整个SQL语句得到处理之后才进行检查。也有一些可延迟的约束允许将完整性约束的验证延迟到应用请求时(发出一个 SET  CONSTRAINTS  ALL  IMMEDIATE 命令)才完成,或者延迟到发出

COMMIT时再检查。

8.3.1 MMEDIATE 约束

这是一般情况。在这种情况下,完整性约束会在整个SQL语句得到处理之后立即检查。注意,这里我用的是“SQL 语句”而不只是“语句”。如果一个 PL/SQL 存储过程中有多条 SQL 语句,那么在每条 SQL 语句执行之后都会立即验证其完整性约束,而不是在这个存储过程完成后才检查它。 那么,为什么约束要在 SQL 语句执行之后才验证呢?为什么不是在 SQL 语句执行期间验证?这是因为,一条语句可能会使表中的各行暂时地“不一致”(如SQL语句:update t set x=x+1),这是很自然的。尽管一条语句全部完成后的最终结果是对的,但如果查看这条语句所做的部分工作,会导致 Oracle拒绝这个结果。

 

8.3.2 DEFERRABLE 约束和级联更新

Oracle8.0开始,我们还能够延迟约束检查,对于许多操作来说,这很有好处。首先能想到的是,可能需要将一个主键的UPDATE级联到子键。也许很多人会说:这没有必要,因为主键是不可变的(我就是这些人之一),但是还有人坚持要有级联 UPDATE。有了可延迟的约束,就使得级联更新成为可能。

注意创建子表的语法如下(父表的创建略):

ops$tkyte@ORA10G> create table c

2 ( fk constraint c_fk

3 references p(pk)

4 deferrable

5 initially immediate

6 )

7 /

Table created.

这个约束创建为一个DEFERRABLE约束,但是设置为 INITIALLY  IMMEDIATE。这说明,可以把这个约束延迟到 COMMIT 或另外某个时间才检查。不过,默认情况下,这个约束在语句级验证。这是可延迟约束最常见的用法。大多数现有的应用不会在 COMMIT 语句上检查约束冲突,你最好也不要这么做。

迟延更新的方法如下:

ops$tkyte@ORA10G> set constraint c_fk deferred;

Constraint set.

ops$tkyte@ORA10G> update p set pk = 2;

1 row updated.

Commit complete. ops$tkyte@ORA10G> update c set fk = 2;

1 row updated.

ops$tkyte@ORA10G> set constraint c_fk immediate;

Constraint set.

ops$tkyte@ORA10G> commit;

这就是级联更新的做法。注意,要延迟一个约束,必须这样来创建它们:先将其删除,再重新创建约束,这样才能把不可延迟的约束改变为可延迟约束。

 

8.4   不好的事务习惯

一般的数据库认为锁是稀有资源,另外读取器会阻塞写入器,反之,写入器也会阻塞读取器。为了提高并发性,这些数据库希望你的事务越小越好,有时甚至会以数据完整性为代价来做到这一点。Oracle 则采用了完全不同的方法。事务总是隐式的,没有办法“自动提交”事务,除非应用专门实现。在Oracle中,每个事务都应该只在必要时才提交,而在此之前不能提交。事务的大小要根据需要而定。锁、阻塞等问题并不是决定事务大小的关键,数据完整性才是确定事务大小的根本。锁不是稀有资源,并发的数据读取器和数据写入器之间不存在竞争问题。这样在数据库中就能有健壮的事务。这些事务不必很短,而要根据需求有足够长的持续时间(但是不能不必要地太长)。事务不是为了方便计算机及其软件,而是为了保护你的数据。

一般不好的思维习惯:

l         频繁地提交大量小事务比处理和提交一个大事务更快,也更高效。

l         没有足够的undo空间。

但其实最好的方法是按业务过程的要求以适当的频度提交,并且相应地设置undo段大小。

 

8.4.1 在循环中提交

1. 性能影响

    如果进行大批量的更新删除操作,最好用一条SQL语句完成,而不要受惯性思维的影响进行批量的提交(如每100条提交一次),这样性能反而不好。

    下面再对这个讨论做个补充,给出一个对应的例子。应该记得在第7章中,我们讨论过写一致性的概念,并介绍了UPDATE语句如何导致重启动。 如果要针对一个行子集 (有一个WHERE  子句)执行先前的UPDATE语句,而其他用户正在修改这个UPDATEWHERE子句中使用的列,就可能需要使用一系列较小的事务而不是一个大事务,或者更适合在执行大量更新之前先锁定表。这样做的目标是减少出现重启动的机会。如果要UPDATE表中的大量行,这会导致我们使用LOCK TABLE命令。不过,根据我的经验,这种大量更新或大量删除(只有这些语句才可能遭遇重启动)都是独立完成的。一次性的大量更新或清除旧数据通常不会在活动高发期间完成。实际上,数据的清除根本不应受此影响,因为我们一般会使用某个日期字段来定位要清除的信息,而其他应用不会修改这个日期数据。

 

2. Snapshot Too Old错误

如果像前面的那个例子一样,你一边在读取表,一边在修改这个表,就会同时生成查询所需的 undo 信息。UPDATE 生成了undo信息,你的查询可能会利用这些undo信息来得到待更新数据的读一致视图。如果提交了所做的更新,就会允许系统重用刚刚填写的undo段空间。如果系统确实重用了undo段空间,擦除了旧的undo数据(查询随后要用到这些undo信息) ,你就有大麻烦了。SELECT会失败,UPDATE也会中途停止。(为什么?)这样就有了一个部分完成的逻辑事务,而且可能没有什么好办法来重启动

ORA-01555错误的例子会使更新处于一种完全未知的状态。有些工作已经做了,而有些还没有做。如果在游标的FOR循环中提交,要想避免ORA-01555,我绝对是无计可施。 ORA-30036 错误是可以避免的,只需在系统中分配适当的资源。通过设置正确的大小就可以避免这个错误;但是第一个错误(ORA-01555)则不然。另外,即使我未能避免 ORA-30036 错误,至少更新会回滚,数据库还是处于一种已知的一致状态,而不会半路停在某个大更新的中间。另外可以通过视图V$UNDOSTAT观察所生成的UNDO数量。

 

3. 可重启动的过程需要复杂的逻辑

如果采用“在逻辑事务结束之前提交”的方法,最验证的问题是:如果 UPDATE 半截失败了,这会经常将你的数据库置于一种未知的状态中。除非你提取对此做了规划,否则很难重启动这个失败的过程,让它从摔倒的地方再爬起来。所以要考虑编写可重启动的逻辑程序,但这可能会很复杂。

推荐的做法:力求简单。如果能在SQL中完成,那就在SQL里完成。如果不能在SQL中完成,就用PL/SQL实现。要用尽可能少的代码来完成,另外应当分配充分的资源。一定要考虑到万一出现错误会怎么样。应当正确地设置 undo 段的大小,比起编写一个可重启动的程序来说,前者要容易得多。

 

8.1 分布式事务

Oracle 有很多很好的特性,其中之一就是能够透明地处理分布式事务。Oracle中分布式事务的关键是数据库链接 (database link)。它使用了一个 2PC 协议来做到这一点。2PC 是一个分布式协议,如果一个修改影响到多个不同的数据库,2PC允许原子性地提交这个修改。在多个数据库之间的一个2PC事务中,其中一个数据库(通常是客户最初登录的那个数据库)会成为分布式事务的协调器。这个站点会询问其他站点是否已经准备好提交。实际上,这个站点会转向其他站点,问它们是否准备就绪。其他的每个站点会报告它的“就绪状态“(YESNO) 。如果只要有一个站点投票 NO,整个事务就会回滚。如果所有站点都投票 YES,站点协调器会广播一条消息,使每个站点上的提交成为永久性的。

    对于分布式事务,还存在一些限制(不过并不多),其中重要的限制如下:

l         不能在数据库链接上发出COMMIT。也就是说,不能发出COMMIT@remote_site。只能从发起事务的那个站点提交。

l         不能在数据库链接上完成 DDL。这是上一个问题带来的直接结果。DDL会提交,而除了发起事务的站点外,你不能从任何其他站点提交,所以不能在数据库链接上完成DDL

l         不能在数据库链接上发出SAVEPOINT。简单地说,不能在数据库链接发出任务事务控制语句。所有事务控制都有最初打开数据库链接的会话继承得来;对于事务中的分布式实例,不能有不同的事务控制。

 

8.5   自治事务

自治事务(autonomous transaction)允许你创建一个“事务中的事务” ,它能独立于其父事务提交或回滚。 自治事务提供了一种用PL/SQL控制事务的新方法,可以用于:

l         顶层匿名块;

l         本地(过程中的过程) 、独立或打包的函数和过程;

l         对象类型的方法;

l         数据库触发器。

例如:

ops$tkyte@ORA10G> create or replace procedure Autonomous_Insert

2 as

3 pragma autonomous_transaction;

4 begin

5 insert into t values ( 'Autonomous Insert' );

6 commit;

7 end;

8 /

Procedure created.

 

注意这里使用了pragma AUTONOMOUS_TRANSACTION。这个指令告诉数据库:执行这个过程时要作为一个新的自治事务来执行,而且独立于其父事务。

注意:pragma是一个编译器指令,这是一种编辑器执行某种编译选项的方法。还有其他一些pragma。参考PL/SQL编程手册,可以看到其索引中有pragma的一个列表。

 

总结一下,如果在一个“正常”的过程中 COMMIT,它不仅会持久保留自己的工作,也会使该会话中未完成的工作成为永久性的。不过,如果在一个自治事务过程中完成COMMIT,只会让这个过程本身的工作成为永久性的。

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值