学习《Oracle 9i10g编程艺术》的笔记 (十一) 事务

 1.事务概述

事务(Transaction)是数据库区别于文件系统的特性之一。在文件系统中,如果你正把文件写到一
半,操作系统突然崩溃了,这个文件就很可能会被破坏。不错,确实还有一些“日报式”(journaled)之
类的文件系统,它们能把文件恢复到某个时间点。不过,如果需要保证两个文件同步,这些文件系统就无
能为力了。倘若你更新了一个文件,在更新完第二个文件之前,系统突然失败了,你就会有两个不同步的
文件。
这是数据库中引入事务的主要目的:事务会把数据库从一种一致状态转变为另一种一致状态。这就是
事务的任务。在数据库中提交工作时,可以确保要么所有修改都已经保存,要么所有修改都不保存。另外,
还能保证实现了保护数据完整性的各种规则和检查。
在上一章中,我们从并发控制角度讨论了事务,并说明了在高度并发的数据访问条件下,根据Oracle
的多版本读一致模型,Oracle 事务每次如何提供一致的数据。Oracle 中的事务体现了所有必要的ACID 特
征。ACID 是以下4 个词的缩写:
原子性(atomicity):事务中的所有动作要么都发生,要么都不发生。
一致性(consistency):事务将数据库从一种一致状态转变为下一种一致状态。
隔离性(isolation):一个事务的影响在该事务提交前对其他事务都不可见。
持久性(durability):事务一旦提交,其结果就是永久性的。

 

2.事务控制语句

Oracle 中不需要专门的语句来“开始事务”。隐含地,事务会在修改数据的第一条语句处开始(也就
是得到TX 锁的第一条语句)。也可以使用SET TRANSACTION 或DBMS_TRANSACTION 包来显示地开始一个事务,
但是这一步并不是必要的,这与其他的许多数据库不同,因为那些数据库中都必须显式地开始事务。如果
发出COMMIT 或ROLLBACK 语句,就会显式地结束一个事务。
注意ROLLBACK TO SAVEPOINT 命令不会结束事务!正确地写为ROLLBACK(只有这一个词)才能结束
事务。
一定要显式地使用COMMIT 或ROLLBACK 来终止你的事务。

 

COMMIT:要想使用这个语句的最简形式,只需发出COMMIT。也可以更详细一些,写为COMMIT
WORK,不过这二者是等价的。COMMIT 会结束你的事务,并使得已做的所有修改成为永久性的(持
久保存)。COMMIT 语句还有一些扩展用于分布式事务中。利用这些扩展,允许增加一些有意义的
注释为COMMIT 加标签(对事务加标签),以及强调提交一个可疑的分布式事务。
ROLLBACK:要想使用这个语句的最简形式,只需发出ROLLBACK。同样地,你也可以罗嗦一
些,写为ROLLBACK WORK,但是二者是等价的。回滚会结束你的事务,并撤销正在进行的所有未
提交的修改。为此要读取存储在回滚段/undo 段中的信息,并把数据库块恢复到事务开始之前的
状态(后面我将把回滚段/undo 段统称为undo 段,Oracle 10g 中都喜欢用这个词)。
SAVEPOINT:SAVEPOINT 允许你在事务中创建一个“标记点”(marked point),一个事务中
可以有多个SAVEPOINT。
ROLLBACK TO <SAVEPOINT>:这个语句与SAVEPOINT 命令一起使用。可以把事务回滚到标记
点,而不回滚在此标记点之前的任何工作。所以,可以发出两条UPDATE 语句,后面跟一个
SAVEPOINT,然后又是两条DELETE 语句。如果执行DELETE 语句期间出现了某种异常情况,而且
你捕获到这个异常,并发出ROLLBACK TO SAVEPOINT 命令,事务就会回滚到指定的SAVEPOINT,
撤销DELETE 完成的所有工作,而UPDATE 语句完成的工作不受影响。
SET TRANSACTION:这条语句允许你设置不同的事务属性,如事务的隔离级别以及事务是只
读的还是可读写的。使用手动undo 管理时,还可以使用这个来指示事务使用某个特定的undo
段,不过不推荐这种做法。

 

3.原子性

3.1语句级原子性

考虑以下语句:

Insert into t values ( 1 );
看上去很明显,如果它由于一个约束冲突而失败,这一行就不会插入。不过,再考虑下面的例子,这
里表T 上的一个INSERT 或DELETE 会触发一个触发器,它将适当地调整表T2 中的CNT 列。

如果触发器触发之后出现了错误,触发器的影响是
否还存在?也就是说,如果触发器被触发,并且更新了T2,但是这一行没有插入到T 中,结果会是什么?

幸运的是,在Oracle
中,客户最初发出的语句(在这里就是INSERT INTO T)会完全成功或完全失败。这个语句是原子性的。

 

Oracle 保证最初的INSET(即导致触发器触发的插入语句)是原子性的,
这个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 或SQL Server 的程序员来说,刚开始可能会有点摸不着头脑。在这些数据
库中,情况恰恰相反。这些系统中的触发器会独立于触发语句执行。如果触发器遇到一个错误,它必须显
式地回滚自己的工作,然后产生另外一个错误来回滚触发语句。否则,即使触发语句(或该语句的另外某
个部分)最终会失败,触发器完成的工作也会持久保留。
在Oracle 中,这种语句级原子性可以根据需要延伸。在前面的例子中,如果INSERT INTO T 触发了
一个触发器,这个触发器会更新另一个表,而那个表也有一个触发器,它会删除第三个表(以此类推),那
么要么所有工作都成功,要么无一成功。为保证这一点,无需你编写任何特殊的代码,Oracle 本来就会这
么做。

 

3.2过程级原子性

Oracle 把PL/SQL 匿名块也当作是语句。请考虑以下存储过程:
Savepoint statement2;
Insert into t values ( -1 );
If error then rollback to statement2;
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;
7 /
Procedure created.
ops$tkyte@ORA10G> select * from t;
no rows selected
ops$tkyte@ORA10G> select * from t2;
CNT

以上创建了一个过程,而且我们知道这个过程不会成功。在这个过程中,第二个INSERT 总会失败。
下面看运行这个存储过程时会发生什么情况:

ops$tkyte@ORA10G> begin
2 p;
3 end;
4 /
I fired and updated 1 rows
I fired and updated 1 rows
begin
*
ERROR at line 1:
ORA-02290: check constraint (OPS$TKYTE.SYS_C009598) violated
ORA-06512: at "OPS$TKYTE.P", line 5
ORA-06512: at line 2
ops$tkyte@ORA10G> select * from t;
no rows selected
ops$tkyte@ORA10G> select * from t2;
CNT
----------
0

可以看到,Oracle 把这个存储过程调用处理为一个原子语句。客户提交了一个代码块BEGIN P; END;,

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

ops$tkyte@ORA10G> begin
2 p;
3 exception
4 when others then null;
5 end;
6 /
I fired and updated 1 rows
I fired and updated 1 rows
PL/SQL procedure successfully completed.
ops$tkyte@ORA10G> select * from t;
X
----------

1
ops$tkyte@ORA10G> select
CNT
----------
1

在此,我们运行的代码块会忽略所有错误,这两个代码块的输出结果有显著的差别。尽管前面第一个
P 调用没有带来任何改变,但在这里的P 调用中,第一个INSERT 会成功,而且T2 中的CNT 列会相应地递
增。
注意
如果代码中包含一个WHEN OTHERS 异常处理器,但其中没有一个RAISE 来重新引发异常,我认
为这样的代码都是有bug 的。它会悄悄地忽略错误,这就改变了事务的语义。如果捕获WHEN
OTHERS,并把异常转换为旧式的返回码,这会改变数据库本该有的表现。

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

。对此可以用另一种方式编写代码,将语句级原子性恢复为整个PL/SQL 块
级原子性,如下所示:
ops$tkyte@ORA10G> begin
2 savepoint sp;
3 p;
4 exception
5 when others then
6 rollback to sp;
7 end;
8 /

前面的代码代表着一种极其糟糕的实践。一般来讲,不应该捕获WHEN OTHERS,另外对于事务
语义而言,也不应该为Oracle 已经提供的特性显式编写代码。

3.3事务级原子性

最后,还有一种事务级原子性的概念。事务(也就是一组SQL 语句作为一个工作单元一同执行)的总
目标是把数据库从一种一致状态转变为另一种一致状态。

4.完整性约束和事务

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

IMMEDIATE 模式:这也是一般情况。在这种情况下,完整性约
束会在整个SQL 语句得到处理之后立即检查。注意,这里我用的是“SQL 语句”而不只是“语句”。如果一
个PL/SQL 存储过程中有多条SQL 语句,那么在每条SQL 语句执行之后都会立即验证其完整性约束,而不是
在这个存储过程完成后才检查它。

5.和其他数据库的区别

如果开发人员使用过另外某个数据库,其中只是“支
持”事务,而没有“提升”事务的使用,执行开发人员就常常有这样的一些坏习惯。例如,在Informix(默
认设置)、Sybase 和SQL Server 中,必须显式地BEGIN(开始)一个事务;否则,每条单个的语句本身就
是一个事务。Oracle 在具体的语句外包了一个SAVEPOINT,采用类似的方式,那些数据库则在各条语句外
包了一个BEGIN WORK/COMMIT 或ROLLBACK。这是因为,在这些数据库中,锁是稀有资源,另外读取器会阻
塞写入器,反之,写入器也会阻塞读取器。为了提高并发性,这些数据库希望你的事务越小越好,有时甚
至会以数据完整性为代价来做到这一点。
Oracle 则采用了完全不同的方法。事务总是隐式的,没有办法“自动提交”事务,除非应用专门实
现(更多详细内容请见8.4.2 节)。在Oracle 中, 每个事务都应该只在必要时才提交,而在此之前不能提
交。事务的大小要根据需要而定。锁、阻塞等问题并不是决定事务大小的关键,数据完整性才是确定事务
大小的根本。锁不是稀有资源,并发的数据读取器和数据写入器之间不存在竞争问题。这样在数据库中就
能有健壮的事务。这些事务不必很短,而要根据需求有足够长的持续时间(但是不能不必要地太长)。事
务不是为了方便计算机及其软件,而是为了保护你的数据。

6.不好的事务习惯

6.1在循环中提交

6.2使用自动提交   在JDBC 中打开一个连接之后紧接着应该有:conn.setAutoCommit (false);

7.分布式事务

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值