Oracle数据库隔离级别,特性,问题和解决方法(一)

2014-11-24 17:06:40 · 作者: · 浏览: 2
Oracle数据库隔离级别,特性,问题和解决方法
如果没有任何数据库隔离策略,在多用户(多事务)并发时,会产生下列问题:
  - 丢失更新(lost update):两个事务同时更新同一条数据时,会发生更新丢失。
例如:用户A读取学号为107的学生(学号=107,姓名=“小明”,年龄=28)
=> 用户B读取学号为107的学生(学号=107,姓名=“小明”,年龄=28)
=> 用户A把姓名更改为“王小明”(学号=107,姓名=“王小明”,年龄=28)
=> 用户B把年龄更改为33(学号=107,姓名=“小明”,年龄=33)
=> 用户A提交(学号=107,姓名=“王小明”,年龄=28)
=> 用户B提交(学号=107,姓名=“小明”,年龄=33)
用户A对学生姓名的更新丢失了。
  - 脏读(dirty read):当一个事务读取另一个事务尚未提交的修改时,产生脏读。
例如:用户A读取学号为107的学生(学号=107,姓名=“小明”,年龄=28)
=> 用户A把姓名更改为“王小明”(学号=107,姓名=“王小明”,年龄=28)
=> 用户B读取学号为107的学生(学号=107,姓名=“王小明”,年龄=28)
=> 用户A撤销更改,事务回滚(学号=107,姓名=“小明”,年龄=28)
这样用户B相当于读取了一个从未存在过的数据“王小明”。如果涉及到金额的话问题更为严重,因为用户B读取了一个金额之后,很可能把这个金额与其它金额累加,再把结果保存到汇总数据之中,这样在月底对不上账的时候,由于用户A回滚了事务,数据库内不会有任何操作记录,这样用户B是何时、从哪里读取了错误数据根本无从查起。
  - 不可重现的读取(nonrepeatable read):同一查询在同一事务中多次进行,在此期间,由于其他事务提交了对数据的修改或删除,每次返回不同的结果。
例如:假设学生表里只有“小明”和“小丽”2条记录(小明.年龄=20,小丽.年龄=30)。如果用户A把“小明.年龄”更改为40、把“小丽.年龄”更改为50并提交事务。在用户A修改数据并提交前,学生的平均年龄为(20+30)/2=25;在用户A修改数据并提交后,学生的平均年龄为(40+50)/2=45。
现在考虑在用户A更新 2 名学生的年龄时,用户B执行了一个计算平均年龄的事务:

如前所述,在用户A修改数据并提交前,学生的平均年龄为(20+30)/2=25;在用户A修改数据并提交后,学生的平均年龄为(40+50)/2=45。用户B计算得出的平均年龄是 25 或 45 都是可以接受的,但是在上例中用户B计算得出的平均年龄 35 是从未出现在系统中的错误数值。
  - 幻读(phantom read):同一查询在同一事务中多次进行,由于其他提交事务所做的插入操作,虽然查询条件相同,每次返回的结果集却不同。

事务B使用相同的条件进行了2次查询/筛选,一次是为了向费用结算表插入汇总数据,一次为了确定对费用明细表的更新范围。在这两次筛选之间,事务A提交了一条新的费用明细数据,导致两次筛选的结果不一致。

隔离级别

  为避免上述并发问题,ANSI/ISO SQL92标准定义了一些隔离级别:
  - 读取未提交数据(read uncommitted)
  - 读取已提交数据(read committed)
  - 可重现的读取(repeatable read)
  - 序列化(serializable)
通过指定不同的隔离级别,可避免上述一种或多种并发问题,见下图。

细心的读者可能已经注意到上图不包括“丢失更新(lost update)”,这是因为“丢失更新”问题需要使用乐观锁或悲观锁来解决,超出本文范围,先不详述。

Oracle 的隔离级别

  SQL92定义的隔离级别在理论上很完善,但是 Oracle 显然认为在实际实现的时候并不应该完全照搬SQL92的模型。
  - Oracle不支持 SQL92 标准中的“读取未提交数据(read uncommitted)”隔离级别,想要脏读都没可能。
  - Oracle 支持 SQL92 标准中的“读取已提交数据(read committed)”隔离级别,(这也是Oracle默认的隔离级别)。
  - Oracle不支持 SQL92 标准中的“可重现的读取(repeatable read)”隔离级别,要想避免“不可重现的读取(nonrepeatable read)”可以直接使用“序列化(serializable)”隔离级别。
  - Oracle 支持 SQL92 标准中的“序列化(serializable)”隔离级别,但是并不真正阻塞事务的执行(这一点在后文还有详述)。
  - Oracle 还另外增加了一个非SQL92标准的“只读(read-only)”隔离级别。

Oracle的序列化(serializable)隔离级别

  序列化,顾名思义,是让并发的事务感觉上是一个挨一个地串行执行的。之所以说是“感觉上”,是因为当2个事务并发时,Oracle并不会阻塞其中一个事务去等待另一个事务执行完毕再执行,而是仍然让2个事务同时并行,那么如何能“感觉”是串行的呢?请看下图的实验。

用户B的事务因为指定了serializable隔离级别,所以虽然在查询费用明细表之前,用户A提交了对费用明细表的更改,但是因为用户A提交的更改是在用户B的事务开始之后才提交的,所以这个更改对用户B的事务不可见。也就是说,用户B的事务开始之后,其它事务提交的更改都不会再影响事务内的查询结果,这样感觉上用户A的事务好像是在用户B的事务结束之后才执行的似的。这本来是非常好的一个特性,极大地提高了并行性,但是也会造成问题。

  问题1:Oracle的这种“假串行”会让严格依赖于时间的程序产生混乱。

  请看下图这个例子,对费用结算的例子稍稍做了一点改动。

程序员的本意是统计2012-3-4这天从零点至运行程序之时的费用总额。如果他以为 Oracle 的 serializable 会像 C# 的 lock 一样阻塞其它事务的话,就会对结果非常吃惊:在2012-3-4 0:00 ~ 2012-3-4 10:02 实际有3条费用明细,总额为20+30+100=150,而不是用户B的事务统计得出的50。

  问题2:ORA-08177 Can't serialize access for this transaction (无法序列化访问)错误。

  如果你使用了 serialize 隔离级别,没准你的客户会经常抱怨这个随机出现的错误。兄弟,你并不孤独!
  导致这个错误的原因有2个:
  (1) 两个事务同时更新了同一条数据。你可以这样重现这个错误:事务B开始(使用serialize 隔离级别) => 事务A开始,更新 表1.RowA 但不提交 => 事务B更新表1.RowA,因为行锁定而被阻塞 => 事务A提交 => 事务B报 ORA-08177 错误。
  (2) 事务所更新的表的 initrans 参数太小。Oracle 官方文档的说法是,如果使用了 serialize 隔离级别,表的 initrans 参数最小要设置成3(默认是1)。
alter table 费用明细表 initrans 3;
  原文:“Oracle Database stores control information in each data block to manage access by concurrent trans