并发与多版本
7.1 什么是并发控制?
并发控制(concurrency control)是数据库提供的函数集合,允许多个人同时访问和修改数据。
多版本是指, Oracle能同时物化多个版本的数据,这也是Oracle提供数据读一致视图的机制 (读一致视图即 read-consistent view,是指相对于某个时间点有一致的结果)。多版本有一个很好的副作用,即数据的读取器(reader)绝对不会被数据的写入器(writer)所阻塞。换句话说,写不会阻塞读。这是 Oracle 与其他数据库之间的一个根本区别。
默认情况下,Oracle的读一致性多版本模型应用于语句级(statement level),也就是说,应用于每一个查询;另外还可以应用于事务级(transaction level) 。这说明,至少提交到数据库的每一条 SQL 语句都会看到数据库的一个读一致视图,如果你希望数据库的这种读一致视图是事务级的(一组SQL语句) ,这也是可以的。
7.2 事务隔离级别
这些隔离级别是根据3个“现象”定义的,以下就是给定隔离级别可能允许或不允许的3种现象:
l 脏读(dirty read) :你能读取未提交的数据,也就是脏数据。只要打开别人正在读写的一个OS文件(不论文件中有什么数据),就可以达到脏读的效果。如果允许脏读,将影响数据完整性,另外外键约束会遭到破坏,而且会忽略惟一性约束。
l 不可重复读(nonrepeatable read) :这意味着,如果你在 T1 时间读取某一行,在 T2时间重新读取这一行时,这一行可能已经有所修改。也许它已经消失,有可能被更新了,等等。也就是说,在同一个事务中重复读取同一行可能返回不同的答案(我认为这称之为“重复读不一致”更容易理解)
l 幻像读(phantom read) :这说明,如果你在 T1 时间执行一个查询,而在 T2 时间再执行这个查询,此时可能已经向数据库中增加了另外的行,这会影响你的结果。与不可重复读的区别在于:在幻像读中,已经读取的数据不会改变,只是与以前相比,会有更多的数据满足你的查询条件。也就是说与事务早期相比,查询不光能看到已经提交的行,还可以看到新插入的行。
SQL隔离级别是根据以下原则定义的,即是否允许上述各个现象。
ANSI隔离级别
隔离级别 脏读 不可重复 幻像读
READ UNCOMMITTED 允许 允许 允许
READ COMMITTED 允许 允许
REPEATABLE READ 允许
SERIALIZABLE
Oracle明确地支持READ COMMITTED(读已提交)和SERIALIZABLE(可串行化)隔离级别。还提供了另外一个级别,称为 READ ONLY(只读)。READ ONLY 事务相对于无法在SQL中完成任何修改的REPEATABLE READ或SERIALIZABLE事务。如果事务使用READ ONLY隔离级别,只能看到事务开始那一刻提交的修改,但是插入、更新和删除不允许采用这种模式(其他会话可以更新数据,但是READ ONLY事务不行) 。如果使用这种模式,可以得到REPEATABLE READ和SERIALIZABLE级别的隔离性。
7.2.1 READ UNCOMMITTED
SQL标准定义的READ UNCOMMITTED隔离级别允许脏读。Oracle没有利用脏读,甚至不允许脏读。READ UNCOMMITTED隔离级别的根本目标是提供一个基于标准的定义以支持非阻塞读。
举例说明,如果一个事务修改某一行,但还没有提交,另一个事务读取这一行时,不同的数据库表现不一样,可能会读修改之后的数据,即出现脏读,允许发生这种事主要是为了非阻塞读。但Oracle并不允许这种事情发生,它会在回滚段中重建数据的前映像返回给读的事务,这样读是一致的,不会出现脏读。这里的关键是,脏读不是一个特性:而是一个缺点。Oracle中根本不需要脏读。Oracle完全可以得到脏读的所有好处(即无阻塞),而不会带来任何不正确的结果。
7.2.2 READ COMMITTED
READ COMMITTED隔离级别是指,事务只能读取数据库中已经提交的数据。这里没有脏读,不过可能有不可重复读(也就是说,在同一个事务中重复读取同一行可能返回不同的答案)和幻像读(与事务早期相比,查询不光能看到已经提交的行,还可以看到新插入的行)。在数据库应用中,READ COMMITTED可能是最常用的隔离级别了,这也是Oracle数据库的默认模式,很少看到使用其他的隔离级别。
举例说明,在一般的其他数据库中,一个事务开始一个聚合统计,它查询每一行然后聚合计算,当进行到某一点时,另一个事务在最后的某一行上(即前一个事务还没有查询到这一行)加一个排他锁更新这一行,那么查询到这一行时事务将会阻塞,因为更新还没有提交。当更新提交后查询继续进行,但这时它查询到的数据将是更新之后的数据,与开始查询那一刻相比是不一致的,这样就会出现错误的结果。但在Oracle中并不会这样,非阻塞读并不会以答案不正确作为代价。有时,鱼和熊掌可以兼得。
7.2.3 REPEATABLE READ
REPEATABLE READ的目标是提供这样一个隔离级别,它不仅能给出一致的正确答案,还能避免丢失更新。
1. 得到一致的答案
如果隔离级别是REPEATABLE READ,从给定查询得到的结果相对于某个时间点来说应该是一致的。大多数数据库(不包括Oracle)都通过使用低级的共享读锁来实现可重复读。共享读锁会防止其他会话修改我们已经读取的数据。当然,这会降低并发性。Oracle则采用了更具并发性的多版本模型来提供读一致的答案。使用共享读锁来得到一致答案的副作用之一:数据的读取器会阻塞数据的写入器。不仅如此,在这些系统这,数据的写入器还会阻塞数据读取器。这是共享读锁的另一个副作用:数据的读取器和写入器可能而且经常相互死锁。可以看到,Oracle 中可以得到语句级的读一致性,而不会带来读阻塞写的现象,也不会导致死锁。Oracle 从不使用共享读锁,从来不会。Oracle选择了多版本机制,尽管更难实现,但绝对更具并发性。
2. 丢失更新:另一个可移植性问题
在采用共享读锁的数据库中,REPEATABLE READ的一个常见用途是防止丢失更新。在一个采用共享读锁(而不是多版本)的数据库中,如果启用了REPEATABLE READ,则不会发生丢失更新错误。这些数据库中之所以不会发生丢失更新,原因是:这样选择数据就会在上面加一个锁,数据一旦由一个事务读取,就不能被任何其他事务修改。如此说来,如果你的应用认为 REPEATABLE READ 就意味着“丢失更新不可能发生”,等你把应用移植到一个没有使用共享读锁作为底层并发控制机制的数据库时,就会痛苦地发现与你预想的并不一样。
7.2.4 SEAIALIZABLE
一般认为这是最受限的隔离级别,但是它也提供了最高程度的隔离性。SERIALIZABLE事务在一个环境中操作时,就好像没有别的用户在修改数据库中的数据一样。 我们读取的所有行在重新读取时都肯定完全一样,所执行的查询在整个事务期间也总能返回相同的结果。
Oracle中是这样实现SERIALIZABLE事务的:原本通常在语句级得到的读一致性现在可以扩展到事务级。结果并非相对于语句开始的那个时间点一致,而是在事务开始的那一刻就固定了。换句话说,Oracle 使用回滚段按事务开始时数据的原样来重建数据,而不是按语句开始时的样子重建。
如果其他会话修改了我们也想修改的数据,则会得到 ORA-08177 错误。但是即使你想修改的行尚未被别人修改后,也可能得到一个 ORA-01877 错误。发生ORA-01877错误的原因可能是:包含这一行的块上有其他行正在被修改。(为什么呢?)
注意:前面提到过,Oracle 中还有一种称为 READ ONLY 的隔离级别。它有着 SERIALIZABLE 隔离级别的所有性质,另外还会限制修改。需要指出,SYS用户(或作为SYSDBA连接的用户)不能有READ ONLY或SERIALIZABLE事务。在这方面,SYS很特殊。
如果使用SERIALIZABLE隔离级别,只要保证以下几点就能很有成效:
l 一般没有其他人修改相同的数据
l 需要事务级读一致性
l 事务都很短(这有助于保证第一点)
最后一点要注意,SERIALIZABLE 并不意味着用户执行的所有事务都表现得好像是以一种串行化方式一个接一个地执行。SERIALIZABLE 不代表事务有某种串行顺序并且总能得到相同的结果。尽管按照 SQL 标准来说,这种模式不允许前面所述的几种现象,但不能保证事务总按串行方式顺序执行。
7.2.5 READ ONLY
READ ONLY事务与SERIALIZABLE事务很相似,惟一的区别是READ ONLY事务不允许修改,因此不会遭遇ORA-08177错误。READ ONLY 事务的目的是支持报告需求,即相对于某个时间点,报告的内容应该是一致的。为达到这个目标,就像对单语句一样,也使用了同样的多版本机制。会根据需要从回滚段重新创建数据,并提供报告开始时数据的 8
原样。不过,READ ONLY事务也不是没有问题。在SERIALIZABLE事务中你可能会遇到ORA-08177错误,而在READ ONLY事务中可能会看到ORA-1555:snapshot too old错误。即回滚段会回绕,你需要的那部分回滚段可能已经被另外某个事务占用了。
7.3 多版本读一致性的含义
7.3.1 一种会失败的常用数据仓库技术
7.3.2 解释热表上超出期望的 I/O
在另外一种情况下很有必要了解读一致性和多版本,这就是生产环境中在一个大负载条件下,一个查询使用的 I/O 比你在测试或开发系统时观察到的 I/O 要多得多,而你无法解释这一现象。造成这种现象的原因是:在你测试系统中,由于它是独立的,所以不必撤销事务修改。不过,在生产系统中,读一个给定的块时,可能必须撤销(回滚)多个事务所做的修改,而且每个回滚都可能涉及 I/O来获取undo信息并应用于系统。
7.4 写一致性
7.4.1 一致读和当前读
Oracle处理修改语句时会完成两类块获取。它会执行:
l 一致读(Consistent read):“发现”要修改的行时,所完成的获取就是一致读。
l 当前读(Current read) :得到块来实际更新所要修改的行时,所完成的获取就是当前读。
Oracle更新过程中在获取当前读时如果发现结果与一致读的结果不一样时,Oracle会悄悄地回滚更新,并重启动(假设使用的是READ COMMITTED隔离级别)。如果你使用了SERIALIZABLE隔离级别,此时这个事务就会收到一个ORA-08177: can’t serialize access错误。采用READ COMMITTED模式,事务回滚你的更新后,数据库会重启动更新(也就是说,修改更新相关的时间点),而且它并非重新更新数据,而是进入SELECT FOR UPDATE模式,并试图为你的会话锁住所有WHERE Y=5的行。一旦完成了这个锁定,它会对这些锁定的数据运行UPDATE,这样可以确保这一次就能完成而不必(再次)重启动。
即使语句本身不一定导致重启动,触发器本身也可能导致发生重启动。一般来讲,UPDATE或DELETE语句的WHERE子句中引用的列能用于确定修改是否需要重启动。Oracle使用这些列完成一个一致读,然后以当前模式获取块时,如果检测到任何列有修改,就会重启动这个语句。一般来讲,不会检查行中的其他列。
注意::NEW和:OLD列值在触发器中引用时,也会被Oracle用于完成重启动检查。在触发器中引用:NEW.X 和:OLD.X 时,会比较 X 的一致读值和当前读值,并发现二者不同。这就会带来一个重启动。从触发器将这一列的引用去掉后,就没有重启动了。
所以,对此的原则是:WHERE 子句中查找行所用的列集会与行触发器中引用的列进行比较。行的一致读版本会与行的当前读版本比较,只要有不同,就会重启动修改。
注意:根据这些信息,我们可以进一步理解为什么使用AFTER FOR EACH ROW触发器比使用BEFORE FOR EACH ROW更高效。AFTER触发器不存在这些问题。
1.4.3 为什么重启动对我们很重要?
如果在触发器中做任何非事务性的工作,就会受到重启动的影响。考虑以下影响:
l 考虑一个触发器,它维护着一些PL/SQL全局变量,如所处理的个数。重启动的语句回滚时,对PL/SQL变量的修改不会“回滚“。
l 一般认为,以 UTL_开头的几乎所有函数(UTL_FILE、UTL_HTTP、UTL_SMTP 等)都会受到语句重启动的影响。语句重启动时,UTL_FILE不会“取消“对所写文件的写操作。
l 作为自治事务一部分的触发器肯定会受到影响。语句重启动并回滚时,自治事务无法回滚。
所有这些后果都要小心处理,要想到对于每一个触发器可能会触发多次,或者甚至对根本未被语句更新的行也会触发。
之所以要当心可能的重启动,还有一个原因,这与性能有关。我们一直在使用单行的例子,但是如果你开始一个很大的批更新,而且它处理了前100,000条记录后重启动了会怎么样?它会回滚前100,000行的修改,以SELECT FOR UPDATE模式重启动,在此之后再完成那100,000行的修改。
了解了这一点后,我几乎完全戒除在触发器里使用自治事务,另外开始重新考虑我的一些应用应该如何实现。例如,我不再从触发器直接发送电子邮件;相反,肯定会在我的事务提交之后用 DBMS_JOB 或新的 Oracle 10g调度工具发送电子邮件。这样能是电子邮件的发送是”事务性的“,也就是说,如果导致触发器触发和发送电子邮件的语句重启动了,它完成的回滚就会回滚 DBMS_JOB请求。我修改了在触发器里做的几乎所有非事务性工作,使之在事后的作业中完成,从而得到事务一致性。
Oracle与采用读锁定机制的数据库之间事务、并发性和锁定行为的比较
隔离级别 | 实现 | 写阻塞读 | 读阻塞写 | 死锁 敏感读 | 不正确的查询结果 | 丢失更新 | 锁升级或限制 |
READ UNCOMMITTED | 非Oracle | 否 | 否 | 否 | 是 | 是 | 是 |
READ COMMITTED | 非Oracle | 是 | 否 | 否 | 是 | 是 | 是 |
READ COMMITTED | Oracle | 否 | 否 | 否 | 否 | 否 * | 否 |
REPEATABLE READ | 非Oracle | 是 | 是 | 是 | 否 | 否 | 是 |
SERIALIZABLE | 非Oracle | 是 | 是 | 是 | 否 | 否 | 是 |
SERIALIZABLE | Oracle | 否 | 否 | 否 | 否 | 否 | 否 |