数据并发和一致性介绍
在单用户的数据库中,用户可以修改数据,而不用担心其他用户在同一时间修改相同的数据。但是,在一个多用户的数据库中,多个事务内的语句可以同时更新相同的数据。同时执行的多个事务必须产生有意义且一致的结果。
多用户数据库必须提供以下功能:
- 保证用户可以同时访问数据(数据并发性)
- 保证每个用户看到数据的一致的视图(数据一致性),包括可以看到用户自己的事务所做的更改,和其他用户已提交的事务所做的更改。
为描述当多个事务同时运行时的事务一致性行为,数据库研究人员定义了一种称为可串行性的事务隔离模型。可串行化事务在一种使其看起来好像没有其他用户正在修改数据库中的数据的环境中运作。
虽然事务之间的这种隔离度好像不错,但在可序列化模式下运行许多应用程序可能会严重影响应用程序吞吐量。对并发运行事务的完全隔离可能意味着一个事务无法在某个正在被另一个事务查询的表上执行插入操作。简而言之,现实的考虑通常需要在完美的事务隔离性和性能之间的一个折衷。
Oracle 数据库通过使用多版本一致性模型和各种类型的锁和事务,来维护数据的一致性。通过这种方式,数据库可以向多个并发用户呈现一个数据的视图,每个视图都在某个时间点上是一致的。因为不同版本的数据块可以同时存在,事务可以读取在某个查询请求时间点上的已提交版本数据,并返回符合一个单一时间点的结果。
多版本读一致性
在 Oracle 数据库中,多版本即同时实现数据的多个版本的能力。Oracle 数据库维护多版本读取一致性。
数据库查询具有以下特征:
- 读一致查询
查询所返回的数据已提交的,且关于某个单一时间点一致。
为说明脏读的问题,假设一个事务更新某列的值,但不提交。第二个事务读取此已更新的脏(未提交)值。第一个会话回滚了事务,使该列仍具有其旧值,但第二个事务继续使用更新的值,这会损坏数据库。脏读会破坏数据的完整性、破坏外键、和忽略唯一约束。 - 非阻塞查询
数据的读取者和写入者不会相互阻塞。
语句级读取一致性
Oracle 数据库始终强制执行语句级读取一致性,保证单个查询所返回的数据是已提交的、且关于某个单一时间点一致。
单个 SQL 语句所一致的时间点取决于事务的隔离级别和查询的性质:
- 在读提交隔离级别,该时间点是语句打开的时间。例如,如果一个 SELECT 语句在 SCN 1000 时打开,则此语句一致于 SCN 1000。
- 在可串行化或只读事务隔离级别,该时间点为事务开始的时间。例如,如果一个事务开始于 SCN 1000,且在该事务中有多个 SELECT 语句发生,则每个语句都一致于 SCN 1000。
- 在闪回查询操作(SELECT … AS OF)中,SELECT 语句显式指定时间点。例如,你可以查询某个表在上星期四下午 2 时的数据。
事务级读取一致性
Oracle 数据库还可以为一个事务中的所有查询提供读取一致性,这称为事务级读取一致性。
在这种情况下,事务中的每个语句都看到来自同一时间点(即该事务开始的时间)的数据。
在一个可序列化事务中的多个查询,能看到事务本身所做的更改。例如,某个事务更新了 employees 表,然后其后续查询将看到对 employees 所做的更新。事务级读取一致性产生可重复的读取,且不会产生幻读读。
读取一致性及撤消
为管理多版本的读取一致性模型,当表同时被查询和更新时,数据库必须创建一组读取一致的数据。
Oracle 数据库通过使用撤销数据实现了这一目标。
每当用户修改了数据,Oracle 数据库会创建撤销条目,并写入到撤销段。撤销段包含由未提交事务或最近提交的事务所更改的数据的旧值。因此,同一数据在各个不同时间点上的多个版本,都可以存在于数据库中。数据库可以使用在不同时间点的数据快照,来提供数据读取一致视图,并实现非阻塞查询。
读取一致性在单实例和 Oracle 真正应用集群(Oracle RAC)环境中都可以得到保证。Oracle RAC 使用一种称为缓存融合的“缓存到缓存” 的数据块传输机制,将一个数据库实例中的数据块读取一致映像传送到另一个实例中。
读一致性:示例
这个示例显示了一个查询,在已提交读隔离级别使用撤销数据以提供语句级的读取一致性。

当数据库为某个查询检索数据块时,数据库确保每个块中的数据反映了该查询开始时的内容。数据库根据需要回滚对数据块所做的更改,以将块重建到查询处理开始的状态。
数据库使用一种称为 SCN 的机制,来保证事务的顺序。当 SELECT 语句进入执行阶段时,数据库会确定查询开始执行时所记录的 SCN。在图 9-1 中,该 SCN 为 10023。在事务中的每个查询必须返回在 SCN 10023 时的已提交数据。
在图 9-1 中,其 SCN 大于 10023 的块具有已更改数据,如图中的两个具有 SCN 10024 的块所示。SELECT 语句需要一个与已提交更改块一致的版本。该数据库将当前数据块复制到新的缓冲区,并应用撤消数据,以重新构造块的早期版本。这些重建的数据块被称为一致读取 (CR) 克隆。
在图 9-1 中,数据库创建了两个 CR 克隆:一个块与 SCN 10006 一致,而另一个块与 SCN 10021 一致。数据库为查询返回重建的数据。通过这种方式,数据库可以防止脏读。
读取一致性和事务表
每个段块的块头包含一个感兴趣的事务列表(ITL)。
数据库使用 ITL 来确定当数据库开始修改块时是否某个事务还未提交。
ITL 中的条目描述了哪些事务有被锁定的行,以及块中的哪些行包含提交和未提交的更改。ITL 指向回滚段中的事务表,提供对数据库所做的更改的时间相关信息。
在某种意义上,块头包含影响块中每个行的事务的最近历史记录。CREATE TABLE 和 ALTER TABLE 语句的 INITRANS 参数,控制被保留的交易历史记录条数。
读一致性和延迟插入
一种称为延迟插入的特殊插入类型不使用标准的读一致性机制。
延迟插入使用 MEMOPTIMIZE_WRITE 提示插入到指定为 MEMOPTIMIZE FOR WRITE 的表中。数据库将这些插入缓冲到 large pool 中,而不是 buffer cache 中。数据库不使用 redo 和 undo 跟踪更改。相反,当Space Management Coordinator(SMCO)将缓冲区写入磁盘时,数据库将自动提交更改。不能回滚更改。
延迟插入与常规插入的重要区别在于:
- 驻留在应用程序(假定已提交的大池中)的数据可能会丢失。例如,在将更改保存到磁盘之前,数据库实例可能会失败,即使应用程序报告更改已保存。
- 不允许直接从内存中读取数据。在后台进程将更改写入磁盘之前,写入器无法读取自己的更改。在提交的更改被写到磁盘之前,没有任何读者可以看到它们。
为了避免客户机应用程序丢失数据,应该在写入大池之后在保存一份本地副本。客户端可以使用 DBMS_MEMOPTIMIZE 包跟踪对内存的写操作的持久性,而 DBMS_MEMOPTIMIZE_ADMIN 包则强制数据库对磁盘进行写操作。
锁定机制
通常,多用户数据库使用某种形式的数据锁定,来解决与数据并发性、一致性、和完整性相关的问题。
锁是防止访问同一资源的事务之间的破坏性相互作用的机制。
ANSI/ISO 事务隔离级别
已由 ANSI 和 ISO/IEC 采纳的 SQL 标准,定义了四个事务隔离级别。这些级别对事务处理吞吐量有不同程度的影响。
这些隔离级别根据在同时运行的事务之间必须防止的现象来定义。可预防的现象有:
- 脏读
一个事务读取了已被另一个事务写入、但尚未提交的数据。 - 不可重复读
一个事务重新读取之前曾经读取过的数据,发现另一个已提交的事务已修改或删除了该数据。例如,用户查询某行,然后稍后又查询相同的行,却发现数据已更改。 - 幻读
一个事务重新运行满足某搜索条件的查询,并返回一个行集,发现另一个已提交的事务已插入了满足搜索条件的其他行。
例如,一个事务查询雇员数目。五分钟后它执行相同的查询,但现在人数却增加了一个,这是因为另一个用户为一名新员工插入了一条记录。满足查询条件的数据比之前更多了,但与不可重复读不同,之前读取的数据不会变化。
根据运行在某个特定的隔离级别的事务所允许发生的现象,SQL 标准定义了四个隔离级别。表 9-1 显示了这些级别。
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交 | 可能 | 可能 | 可能 |
| 读已提交 | 不可能 | 可能 | 可能 |
| 可重复读 | 不可能 | 不可能 | 可能 |
| 可串行化 | 不可能 | 不可能 | 不可能 |
Oracle 数据库提供了已提交读(默认值)和可串行化隔离级别。另外,数据库也可以提供一种只读模式。
Oracle 数据库事务隔离级别概述
事务隔离级别的 ANSI 标准定义了在各个隔离级别所允许或必须防止的现象。
读已提交隔离级别
在(默认的)已提交读隔离级别中,事务中执行的每个查询,仅看到在查询开始之前提交的数据——而不是事务开始之前提交的数据。
这个隔离级别是默认的,它适合于几乎不可能发生事务冲突的数据库环境。
已提交读事务中的查询可以避免读取在查询过程中所提交的数据。例如,如果一个查询正扫描到一个百万行表的中间,而另一个不同的事务对第 950000 行提交了一个更新,但当查询读到第 950000 行时,它并不能看见这个变化。但是,因为数据库不会阻止其它事务修改一个查询所读取的数据,其他事务可能会在查询执行期间更改数据。因此,两次运行相同查询的事务可能会遇到模糊读取和幻像读取现象。
在已提交读隔离级别中的读取一致性
数据库为每个查询提供一个一致的结果集,其目的是为了保证数据一致性,而无需用户采取任何行动。
对于隐含的查询(如在一个 UPDATE 语句中的 WHERE 子句),也同样可以保证其一致的结果集。但是,在隐式查询中的每个语句不会看到 DML 语句本身所做的更改,只能看到更改之前所存在的数据。
如果 SELECT 列表中包含一个 PL/SQL 函数,则数据库在该 PL/SQL 函数代码内运行的 SQL 所在语句级别(而不是在父 SQL 级别)上,应用语句级别读取一致性。例如,一个函数可能会访问某个表,其数据被另一个用户更改并提交。在 SELECT 语句中的每次函数运行,都会建立一个新的读一致性快照。
在读提交事务中的写入冲突
在一个读已提交事务中,当事务尝试更改由另一个未提交并发事务所更新的行时,会发生写入冲突。
阻止行修改的事务有时称为阻塞事务。读提交事务将等待阻塞事务结束并释放其行锁。
选项如下所示:
- 如果阻塞事务回滚,正在等待的事务将继续并更改之前被锁定的行,就像另一个事务从未存在一样。
- 如果阻塞事务提交并释放了锁,则正在等待的事务将对这个刚被更新的行继续其预定更新。
下表显示了一个可能是可串行化的或已提交读的事务 1,如何与另一个已提交读的事务 2 进行交互。表 9-2 显示了一个称为丢失更新的典型情况。事务 1 所作的更新不能在表中反映出来,即使事务 1 已经提交它。制定一项策略以处理更新丢失是应用程序开发的一个重要部分。
| Session 1 | Session 2 | Explanation |
|---|---|---|
|
No action. |
会话 1 查询 Banda、Greene、和 Hintz 的薪金。找不到名为 Hintz 的任何雇员。 |
|
No action. |
会话 1 开始一个事务 1,更新 Banda 的薪金。事务 1 的默认隔离级别是已提交读。 |
| No action. | |
会话 2 开始一个事务 2,并将隔离级别显式设置为已提交读。 |
| No action. | |
事务 2 查询 Banda、Greene 和 Hintz 的薪金。Oracle 数据库使用读取一致性,显示事务 1 做出未提交更新前 Banda 的薪金。 |
| No action. | |
事务 2 成功更新 Greene 的薪金,因为事务 1 只锁定了 Banda 所在的行 |
|
No action. |
事务 1 为雇员 Hintz 插入一行,但不提交。 |
| No action. | |
事务 2 查询 Banda、Greene 和 Hintz 的薪金。 事务 2 看到它自己对 Greene 薪金的更新。但事务 2 看不到由事务 1 对 Banda 薪金所做的未提交更新,或为 Hintz 插入的新行。 |
| No action. | |
事务 2 试图更新当前被事务 1 锁定的 Banda 行,这会产生一个写入冲突。事务 2 必须等待,直到事务 1 结束。 |
|
No action. | 事务 1 提交其工作,以结束事务。 |
| No action. | |
现在 Banda 行上的锁释放了,所以事务 2 得以继续,并完成对 Banda 薪金的更新。 |
| No action. | |
事务 2 查询雇员 Banda、Greene 和 Hintz 的薪酬。现在事务 1 已提交了所插入的 Hintz 行,并能被事务 2 看到。事务 2 也看到自己对 Banda 薪金的更新。 |
| No action. | COMMIT; |
事务 2 提交其工作,以结束事务。 |
|
No action. |
会话 1 查询 Banda、Greene、Hintz 的行。Banda 的薪金是 6300,这是事务 2 所作的更新。事务 1 对 Banda 的薪金改至 7000 的更新现在丢失了。 |
可串行化隔离级别
在可串行化隔离级别,事务只看到自事务开始以来(而不是自查询以来)该事务本身所提交的更改。
可串行化事务的运行环境,使其看起来好像没有其他用户在修改数据库中的数据。可串行化隔离适合如下环境:
- 大型数据库中只更新少数几行的短事务
- 两个并发事务将修改相同的行的可能性相对较低
- 较长时间运行的事务主要为只读事务
在可串行化隔离级别,在语句级别所获得的读取一致性通常延伸到整个事务范围。当重新读取在同一事务中之前读取的任何行时,保证结果相同。可以保证任何查询在该事务的持续期间返回相同的结果,因此其他事务所做的更改是不可见的,无论该查询已运行了多长时间。可串行化事务不会遇到脏读、模糊读取、或幻读。
Oracle 数据库允许可串行化事务修改行,只要当可序列化事务开始时,由其它事务对行所做更改已提交。当一个串行化事务试图更新或删除某数据,而该数据在串行化事务开始后被一个不同的事务更改并提交,则数据库将生成一个错误:
ORA-08177: Cannot serialize access for this transaction
当可序列化事务失败,产生 ORA-08177 错误时,应用程序可以采取行动,包括以下几种:
- 将所执行的工作提交到该点
- 也许要先回滚到事务中之前建立的某保存点,然后执行一些其他额外的(不同)语句
- 回滚整个事务
下表显示了一个可串行化事务与其它事务之间的交互。如果可串行化事务不会尝试更改由另一个事务在该可序列化事务开始后所提交的行,就可以避免串行化访问问题。
| Session 1 | Session 2 | 解释 |
|---|---|---|
|
No action. |
会话 1 查询 Banda、Greene、和 Hintz 的薪酬。找不到名为 Hintz 的任何雇员。 |
|
No action. |
会话 1 开始一个事务 1,更新 Banda 的薪金。事务 1 的默认的隔离级别是已提交读。 |
|
No action. |
|
会话 2 开始一个事务 2,并将隔离级别显式设置为可序列化隔离级别。 |
|
No action. |
|
事务 2 查询 Banda、Greene、和 Hintz 的薪金。Oracle 数据库使用读取一致性,显示事务 1 做出未提交更新前 Banda 的薪金。 |
|
No action. |
|
事务 2 成功更新 Greene 的薪金,因为只有 Banda 行被锁定。 |
|
No action. |
事务 1 为雇员 Hintz 插入一行。 |
|
No action. |
事务 1 提交其工作,以结束事务。 |
|
|
会话 1 查询雇员 Banda、Greene、和 Hintz 的薪金,只能看到由事务 1 提交的更改。会话 1 不能看见会话 2 对 Greene 所做的未提交更新。 事务 2 查询雇员 Banda、Greene、和 Hintz 的薪金。Oracle 数据库读取一致性确保由事务 1 所做的插入 Hintz 行和对 Banda 行的更新,对事务 2 不可见。事务 2 只看到自己对 Banda 薪金的更新。 |
No action. |
|
事务 2 提交其工作,结束该事务。 |
|
|
两个会话分别查询雇员 Banda, Greene, 和 Hintz 的薪金。每个会话将看到事务 1 和事务 2 作出的所有已提交更改。 |
|
No action. |
会话 1 更新 Hintz 的薪金,开始事务 3。事务 3 的默认隔离级别是已提交读。 |
No action. |
|
会话 2 开始事务 4,并将其设置为可串行化隔离级别。 |
No action. |
|
事务 4 尝试更新 Hintz 的薪金,但被阻塞,因为事务 3 锁定了 Hintz 行在事务队列中,事务 4 排在事务 3 的后面。 |
|
No action. |
事务 3 提交了它对 Hintz 工资的更新,以结束事务。 |
No action. |
|
结束事务 3 的提交导致事务 4 对 Hintz 更新失败,导致 ORA-08177 错误。该错误之所以发生,是因为事务 3 是在事务 4 开始后提交的 |
No action. |
|
会话 2 回滚事务 4,结束该事务。 |
No action. |
|
会话 2 开始事务 5,并将其设置为可串行化隔离级别。 |
No action. |
|
事务 5 查询 Banda、Greene、和 Hintz 的薪金。事务 3 所提交的对 Hintz 薪酬的更新是可见的。 |
No action. |
|
事务 5 将 Hintz 的薪金更新为一个不同值。因为事务 3 对 Hintz 的更新是在事务 5 开始之前提交的,也就避免了序列化的访问问题。 |
No action. |
|
会话 2 提交了更新,未遇到任何问题,并结束事务。 |
只读隔离级别
只读隔离级别类似于可串行化隔离级别,但只读事务不允许数据在事务中被修改,除非该用户是 SYS。
只读事务不会受到 ORA-08177 错误的影响。只读事务可用于生成报表,其内容必须与事务开始时保持一致。
Oracle 数据库通过按需从撤销段重建数据,来实现读取一致性。因为撤消段是以一个循环方式使用的,数据库可以覆盖撤销数据。长时间运行的报表可能有一定的风险,读取一致性所需要的撤销数据,可能已被一个不同的事务重用,并抛出快照太旧(snapshot too old)错误。设置一个撤消保留期,即在旧数据被覆盖之前,数据库尝试保留撤消数据的最短时间,以期避免这一问题。
数据库锁定机制概述
锁是一种防止破坏性相互作用的机制。
当访问共享数据的事务之间不正确地更新数据或不正确地更改基础数据结构时,交互是破坏性的。锁在维护数据库并发性和一致性当中扮演着一个关键的角色。
锁定行为总结
数据库维护几种不同类型的锁,这取决于获取锁的操作。
一般地,数据库使用两种类型的锁:独占锁和共享锁。在一个资源(如行或表)上,只能获得一个独占锁,但在单个资源上可以获得很多共享锁。
锁会影响读取者与写入者的交互。读取者是一个对某种资源的查询语句,而写入者是一个对某个资源的修改语句。以下规则总结了 Oracle 数据库中读取者和写入者的锁定行为:
- 一行只有在被某个写入者修改时,才被锁定。
当一个语句更新某行时,事务只需要获取在该行上的锁。通过在行级别锁定表数据,数据库最小化对同一数据的争用。在正常情况下,数据库不会将行锁升级到块级或表级。 - 一行的写入者,会阻塞在同一行上的并发写入者。
如果一个事务正在修改某行,则行锁可防止不同的事务同时修改同一行。 - 一个读取者永远不会阻塞一个写入者。
由于行的读取者不会将它锁定,所以一个写入者可以修改该行。唯一的例外是 SELECT … FOR UPDATE 语句,它是一种特殊类型的 SELECT 语句,的确会锁定它正在读取的行。 - 一个写入者绝不会阻塞一个读取者。
当一个行正在被某个写入者更改时,数据库使用撤销数据向读取者提供一个该行的一致视图。
使用锁
在单用户数据库中,锁不是必需的,因为只有一个用户在修改信息。但是,当多个用户在访问和修改数据时,数据库必须提供一种方法,以防止对同一数据进行并发修改。
锁实现了以下重要的数据库需求:
- 一致性
一个会话正在查看或更改的数据不能被其它会话更改,直到用户会话结束。 - 完整性
数据和结构必须按正确的顺序反映对他们所做的所有更改。
Oracle 数据库通过其锁定机制,提供在多个事务之间的数据并发性、一致性、和完整性。锁定将自动执行,并且不需要用户操作。
对锁的需求,可以由对某个单一行的并发更新来说明。在下面的示例中,一个简单的基于 web 的应用程序,将某个雇员的电子邮件和电话号码呈现给最终用户。应用程序使用如下所示的 UPDATE 语句修改该数据:
UPDATE employees
SET email = ?, phone_number = ?
WHERE employee_id = ?
AND email = ?
AND phone_number = ?
在前面的 UPDATE 语句中,WHERE 子句中电子邮件和电话号码是某指定雇员的原始的、未经修改的值。此更新可确保应用程序修改的行,在应用程序之前读取并向用户显示后,没有被更改过。以这种方式,应用程序可避免丢失更新的数据库问题,即其中一个用户覆盖了另一个用户所做的修改,实际上丢失了第二个用户所做的更新(表 9-2 显示了一个丢失更新的示例)。
| T | Session 1 | Session 2 | 解释 |
|---|---|---|---|
| t0 | |
在会话 1 中,用户 hr1 查询在表 hr.employees 中的 Himuro 的记录,并显示其员工 ID(118)、电子邮件(GHIMURO)、和电话号码(515.127.4565)等属性。 | |
| t1 | |
在会话 2 中,用户 hr2 查询在表 hr.employees 中的 Himuro 的记录,并显示其 员工 ID (118)、(GHIMURO)和电话号码(515.127.4565) 等属性。 | |
| t2 | |
在会话 1 中,用户 hr1 对 GHIMURO 所在行,更新其电话号码为 515.555.1234,这会在该行上获取一个行锁。 | |
| t3 | |
在会话 2 中,用户 hr2 尝试更新相同的行,但被阻塞,因为 hr1 当前正在处理该行。 hr2 的更新尝试几乎与 hr1 的更新同时发生。 |
|
| t4 | |
在会话 1 中,用户 hr1 提交其事务。 该提交使对 Himuro 的更改持久化,这也就取消了对一直在等待的会话 2 的阻塞。 |
|
| t5 | |
在会话 2 中,用户 hr2 发现 GHIMURO 行被修改,且不再匹配其谓词。 由于该谓词不匹配,会话 2 未能更新任何记录。 |
|
| t6 | |
在会话 1 中,hr1 用户发现它错误地更新了 GHIMURO 行的电话号码。该用户启动一个新事务,并将该行的电话号码更新为 515.555.1235,这又将锁定 GHIMURO 行。 | |
| t7 | |
在会话 2 中,用户 hr2 查询表 hr.employees 中的 Himuro 的记录。记录显示由会话 1 在 t4 时刻对电话号码所做的已提交更新。Oracle 数据库读取一致性确保会话 2 不会看到会话 1 在 t6 时刻所做的未提交更改。 | |
| t8 | |
在会话 2 中,用户 hr2 尝试更新相同的行,但被阻塞,因为 hr1 当前正在处理该行。 | |
| t9 | |
在会话 1 中,hr1 用户回滚其事务并结束之。 | |
| t10 | |
在会话 2 中,因为会话 1 的更新被回滚,所以这次对电话号码的更新成功了。GHIMURO 行与它的谓词相匹配,因此更新成功了。 | |
| t11 | |
会话 2 提交其更新,以结束事务。 |
执行 SQL 语句时,Oracle 数据库自动获取所需的锁。例如,在数据库允许某个会话修改数据之前,该会话必须先锁定数据。锁给予该会话对数据的独占控制权,以便在释放该锁之前,任何其他事务都不可以修改被锁定的数据。
因为数据库的锁定机制与事务控制紧密地绑定在一起,应用程序设计人员只需要正确地定义事务,而数据库会自动管理锁定。虽然数据库也支持用户能够手动锁定数据,但用户从来就不需要显式锁定任何资源。
以下各节解释了理解数据库如何实现数据并发很重要的概念。
锁模式
Oracle 数据库自动使用最低适用的限制级别,来提供最高程度的数据并发,但还能提供非常安全的数据完整性。
限制级别越低、则有更多的可用数据供其他用户访问。相反,限制级别越高,则其他事务为获取其所需的锁类型就将遭受更多的限制。
Oracle 数据库在多用户数据库中使用两种锁定模式:
- 独占锁模式
此模式可防止相关资源被共享。当一个事务修改数据时,获取一个独占锁。直到独占锁被释放之前,第一个以独占方式锁定资源的事务是唯一可以更改资源的事务。 - 共享锁模式
取决于所涉及的操作,此模式允许相关资源被共享。读取数据的多个用户可以共享数据,并持有共享锁,以防止企图获取独占锁的写入者并发访问。多个事务可以同时获取在同一资源上的共享锁。
假定一个事务使用 SELECT … FOR UPDATE 语句选择一个单一表行。该事务获取一个独占行锁和行共享表锁。行锁允许其他会话修改除锁定行之外的任何行,而表锁可防止其它会话更改表结构。这样,数据库允许尽可能多的语句得以执行。
锁转换和锁升级
Oracle 数据库在必要时执行锁转换。
在锁转换中,数据库自动将较低限制的表锁转换为较高限制的其它锁定。例如,假设事务对某雇员发出 SELECT … FOR UPDATE,并之后更新了该锁定行。在这种情况下,数据库会自动将行共享表锁转换为行独占表锁。一个事务在该事务中所有执行插入、更新、或删除的行上持有行独占锁。因为行锁是在最高程度限制下获得的,因此不要求锁转换,也不执行锁转换。
锁转换不同于锁升级,锁升级发生在当某个粒度级别持有许多锁(例如行),数据库将其提高到更高粒度级别(例如表)。如果一个用户锁定了一个表中的许多行,则某些数据库自动将行锁升级到单个表锁。锁的数量减少了,但被锁定的东西却增加了。
Oracle 数据库永远不会升级锁。锁升级极大地增加了死锁的可能性。假定一个系统尝试升级事务 1 中的锁,但因为事务 2 持有该锁,故不能成功。如果事务 2 在它可以继续操作之前也需要在相同的数据上进行锁升级,则将发生一个死锁。
锁持续时间
当某些事件发生,使事务不再需要资源时,Oracle 数据库会自动释放锁。
通常,数据库持有语句所获取的锁,直至该语句所在事务的整个持续期间结束。这些锁可以防止破坏性干扰,如多个并发事务中的脏读、更新丢失、和破坏性 DDL。
当 Oracle 数据库提交或回滚事务时,会释放事务内的语句获取的所有锁。当 Oracle 数据库回滚至保存点时,也会释放该保存点之后所获取的锁。但是,只有不在等待以前被锁定的资源的事务,可以获取现有可用资源上的锁。等待中的事务会继续等待,直到原始事务提交或完全回滚。
锁和死锁
死锁情况发生在两个或多个用户都在等待被对方锁定的数据时。死锁会阻止某些事务继续工作。
Oracle 数据库自动检测死锁,并通过回滚死锁中的一个语句以释放其冲突行锁,来解决死锁。数据库向遭遇语句级回滚的事务返回一条相应的消息。被回滚语句属于检测到死锁的事务。通常,收到通知的事务应该明确回滚,但它可以等待一会之后重试被回滚的语句。
| T | Session 1 | Session 2 | 解释 |
|---|---|---|---|
| t0 | |
|
会话 1 启动事务 1,并更新雇员 100 的薪酬。会话 2 启动事务 2,并更新雇员 200 的薪酬。没有问题存在,因为每个事务只锁定它会尝试更新的行。 |
| t1 | |
|
事务 1 试图更新员工 200 的行,这当前正由事务 2 锁定。事务 2 试图更新雇员 100 的行,这当前正由事务 1 锁定。 死锁发生了,因为任何一个事务都不能获取其得以继续或终止的资源。无论每个事务等待多久,冲突锁依然被对方持有。 |
| t2 | |
事务 1 发出死锁信号,并回滚在 t1 时刻发出的 UPDATE 语句。但是,在 t0 时刻所做的更新不会回滚。会话 1 返回到提示符。 | |
| t3 | |
会话提交了 t0 时刻所做的更新,以结束事务 1。在 t1 时刻尝试的更新不成功,未能提交。 | |
| t4 | |
执行事务 2 在 t1 时刻所做的更新,之前被事务 1 阻塞,现在得以执行。并返回提示符。 | |
| t5 | |
会话 2 提交其在 t0 和 t1 时刻所做的更新,结束事务 2。 |
当事务显式覆盖 Oracle 数据库的默认锁定时,最经常发生死锁。由于数据库不会升级锁,也不对查询使用读锁定,而是使用行级(而不是页级别)锁定,所以很少会出现死锁。
自动锁的概述
Oracle 数据库会为事务自动锁定资源,以防止其他事务进行某些需要独占访问同一资源的操作。
数据库在不同的限制级别自动获取不同类型的锁,这依赖于不同的资源和正在执行的操作。
Oracle数据库锁分为如下表所示的类别。
| 锁 | 描述 | 参阅 |
|---|---|---|
| DML 锁 | 保护数据。例如,表锁锁定整个表,而行锁锁定所选的行。 | “DML Locks” |
| DDL 锁 | 保护模式对象的结构——例如,表和视图的数据字典定义。 | “DDL Locks” |
| 系统锁 | 保护内部数据库结构,如数据文件。闩锁、互斥体、和内部锁是完全自动的。 | “System Locks” |
DML 锁
DML 锁,也称为数据锁,确保由多个用户并发访问的数据的完整性。
例如,DML 锁可防止两个客户从一个在线书店购买某一本书所剩的最后一个拷贝。DML 锁也可以防止多个相互冲突的 DML 或 DDL 操作产生破坏性干扰。
DML 语句自动获取下列类型的锁:
在下面几节,在每种类型的锁或锁模式后的括号中,首字母缩写词是用于在 Oracle 企业管理器(企业管理器)的锁监视器中的缩写。企业管理器中可能将任何的表锁显示为 TM,而不显示表锁的模式(如 RS 或 SRX)。
行锁(TX)
行锁,也称为 TX 锁,是一个表中单个行上的锁。一个事务在被 INSERT、UPDATE、DELETE、MERGE、或 SELECT … FOR UPDATE 等语句所修改的每一行上获取一个行锁。行锁一直存在直到事务提交或回滚。
行锁主要作为一种排队的机制,以防止两个事务修改相同的行。数据库始终以独占模式锁定修改的行,以便其他事务不能修改该行,直到持有锁的事务提交或回滚。行锁定提供了近乎最细粒度的锁定,并因此提供了近乎最佳的并发性和吞吐量。
如果一个事务在某行上获取了一个锁,则该事务也将在包含该行的表上获取一个锁。表锁可防止冲突性的 DDL 操作在当前事务中会覆盖数据更改。图 9-2 演示了在一个表中第三行上的更新。Oracle 数据库将自动在更新行上置一个独占锁,且在表上置一个次独占锁。

行锁和并发
这个场景说明了 Oracle 数据库如何在并发事务中使用行锁。
3 个会话同时查询相同的行。会话 2 和会话 3 对不同的行进行未提交更新,而会话 3 没有做任何更新。每个会话可以看到自己的未提交更新,但看不到其他会话中所做的任何未提交更新。
| T | Session 1 | Session 2 | Session 3 | Explanation |
|---|---|---|---|---|
| t0 | |
|
|
三个不同的会话同时查询 ID 为 100 和 101 的雇员的薪金。每个查询所返回的结果是相同的。 |
| t1 | |
会话 1 更新雇员 100 的薪金,但不提交。对此更新,写入者只需要对更新的行获得一个行级锁,从而防止其他写入者修改这一行。 | ||
| t2 | |
|
|
每个会话同时发出原来的查询。会话 1 显示了 t1 时刻的薪金更新结果 612。会话 2 和会话 3 中的读取者立即返回行,并不等待会话 1 结束其事务。数据库使用多版本读取一致性将薪金显示为会话 1 更改它之前的样子。 |
| t3 | |
会话 2 更新雇员 101 的工资,但不提交事务。对此更新,写入者只需要对更新的行获得一个行级锁,从而防止其他写入者修改这一行。 | ||
| t4 | |
|
|
每个会话同时发出原来的查询。会话 1 显示了 t1 时刻的薪金更新结果 612,而不显示在会话 2 中为员工 101 所做的的薪金更新。会话 2 中的读取者显示在会话 2 中所做的薪金更新,但不显示会话 1 中所做的薪金更新。在会话 3 中的读取者使用读 取一致性来显示会话 1 和 2 的修改之前的薪金。 |
行锁存储
与某些数据库使用锁管理器在内存中维护一个锁列表不同,Oracle 数据库将锁信息存储在包含锁定行的数据块中。
数据库使用排队机制来获取行锁。如果事务需要为某个未锁定的行获取一个锁,则事务将在数据块上置一个锁。被此事务修改的每个行指向存储在块头中的事务 ID 的一个副本。
当事务结束时,事务 ID 仍然保留在块头中。如果一个不同的事务想要修改某行,则它使用该事务 ID 来确定锁是否处于活动状态。如果锁是活动的,则会话要求该锁被释放时得到通知。否则,事务获取该锁。
表锁™
锁,也称为 TM 锁,当一个表被 INSERT、UPDATE、DELETE、MERGE、带 FOR UPDATE 子句的 SELECT 等修改时,由相关事务获取该锁。
DML 操作需要表锁来为事务保护 DML 对表的访问,并防止可能与事务冲突的 DDL 操作。
表锁可能以下列模式之一持有:
- 行共享 (RS)
这种锁也被称为子共享表锁(SS),表示在表上持有锁的事务在表中有被锁定的行,并打算更新它们。行共享锁是限制最少的表级锁模式,提供在表上最高程度的并发性。 - 行独占表锁 (RX)
这种锁也被称为子独占表锁(SX),通常表示持有锁的事务已更新了表行或发出了 SELECT … FOR UPDATE。一个 SX 锁允许其他事务并发地查询、插入、更新、删除、或锁定在同一个表中的行。因此,SX 锁允许多个事务对同一个表同时获得 SX 和子共享表锁。 - 共享表锁(S)
由某个事务拥有的共享表锁允许其他事务查询(而不使用 SELECT … FOR UPDATE),但是更新操作只能在仅有单个事务持有共享表锁时才允许。因为可能有多个事务同时持有共享表锁,所以持有此锁不足以确保一个事务可以修改该表。 - 共享行独占表锁 (SRX)
这种锁也称为共享子独占表锁(SSX),比共享表锁的限制性更强。一次只能有一个事务可以获取给定的表上的 SSX 锁。由某个事务拥有的 SSX 锁允许其他事务查询该表(除 SELECT … FOR UPDATE)但不能更新该表。 - 独占表锁(X)
这种锁是最严格的锁,禁止其他事务执行任何类型的 DML 语句,或在表上放置任何类型的锁。
锁和外键
Oracle 数据库将与外键相关的父键的并发控制最大化。
锁定行为将取决于外键列是否已被索引。如果外键未被索引,则子表将更可能被频繁锁定,或发生死锁,并降低并发性。为此,外键几乎总是应该被索引的。唯一的例外是当匹配的唯一键或主键永远不会被更新或删除。
锁定和未索引外键
当子表的外键列没有索引存在,且一个会话修改父表的主键(例如,删除行或修改主键属性)或合并行父表时,数据库将为子表获取一个全表锁。
当下列两个条件成立时,数据库在子表上获取一个全表锁定:
- 在子表的外键列上没有索引存在。
- 会话修改了父表中的主键(例如,删除了行或修改了主键属性)或将行合并到父表。
假设 hr.departments 表是 hr.employees 表的父表,hr.employees 表包含未索引的外键 department_id。下图显示了一个修改 departments 表中的部门 60 的主键属性的会话。

在图 9-3 中,数据库在部门 60 的主键修改过程中,在 employees 表上获取了一个全表锁定。此锁使其他会话可以查询 employees 表,但不能更新 employees 表。例如,雇员的电话号码不能更新。一旦 departments 表上的主键修改完成后,employees 表上的表锁立即释放。如果在部门中有多个行被修改主键,则在 departments 表中每修改一行,就需要在 employees 表上获取并释放一次表锁。
锁和索引外键
数据库不会获取子表上的全表锁定,当子表中的外键列已被索引,且会话修改了父表中的主键(例如,删除了行或修改了主键属性)或将行合并到父表时。
父表上的锁可以防止其它事务获取独占表锁,但在主键修改过程中不会阻止父表或子表上的 DML。主键修改发生时,我们仍可以修改子表,这种情形正是我们想要的。
图 9-4 显示一个具有索引列 department_id 的子表 employees。事务从 departments 表中删除部门 280。此删除操作不会像“Locks and Unindexed Foreign Keys”场景所描述那样,导致数据库在 employees 表上获取全表锁定。

如果在子表指定了 ON DELETE CASCADE 选项,则从父表删除记录会导致子表中删除相应记录。例如,删除部门 280 会导致从 employees 表中删属于被删除部门的雇员。在这种情况下,等待和锁定规则等同于您先从父表中删除行,然后从子表删除相应行。
DDL 锁
当某个运行中的 DDL 操作正在操作或引用某模式对象时,数据字典(DDL)锁保护该模式对象的定义。
在 DDL 操作的过程中,只有被修改或引用的单个模式对象被锁定。数据库绝不会锁定整个数据字典。
Oracle 数据库将为任何要求锁的 DDL 事务自动获取 DDL 锁。用户不能显式请求 DDL 锁。例如,如果用户创建一个存储过程,则数据库自动为过程定义中引用的所有模式对象获取 DDL 锁。DDL 锁防止在过程编译完成之前,这些对象被更改或删除。
独占 DDL 锁
独占 DDL 锁可防止其他会话获取 DDL 或 DML 锁。
绝大多数 DDL 操作需要对资源获取独占锁,以防止和其他可能会修改或引用相同模式对象的 DDL 之间的破坏性干扰。例如,当 ALTER TABLE 正在将一列添加到表时,不允许 DROP TABLE 删除表,反之亦然。
独占 DDL 锁在整个 DDL 语句执行期间一直持续,并自动提交。在独占 DDL 锁获取过程中,如果另一个操作在该模式对象上持有另一个 DDL 锁,则这个锁获取将一直等待,直到前一个 DDL 锁被释放,才能继续。
共享 DDL 锁
在资源上的共享 DDL 锁可防止与冲突的 DDL 操作发生破坏性干扰,但允许类似的 DDL 操作的数据并发。
例如,当 CREATE PROCEDURE 语句运行时,所在事务将为所有被引用的表获取共享 DDL 锁。其他事务可以同时创建引用相同表的过程,并在相同的表上同时获得共享 DDL 锁,但没有任何事务能在任何被引用表上获取独占 DDL 锁。
共享 DDL 锁在整个 DDL 语句执行期间持续存在,并自动提交。因此,持有一个共享 DDL 锁的事务,可保证在事务过程中,被引用模式对象的定义保持不变。
可中断的解析锁
SQL 语句或 PL/SQL 程序单元,为每个被其引用的模式对象持有一个解析锁。
获取解析锁的目的是,如果被引用的对象被更改或删除,可以使相关联的共享 SQL 区无效。解析锁被称为可中断的解析锁,因为它并不禁止任何 DDL 操作,并可以被打破以允许冲突的 DDL 操作。
解析锁是在执行 SQL 语句的分析阶段,在共享池中获取的。只要该语句的共享 SQL 区仍保留在共享池中,该锁就一直被持有。
系统锁
Oracle 数据库使用各种类型的系统锁,来保护数据库内部和内存结构。由于用户不能控制其何时发生或持续多久,这些机制对于用户几乎是不可访问的。
闩锁
闩锁是简单、低级别的串行化机制,用于协调对共享数据结构、对象、和文件的多用户访问。
闩锁防止共享内存资源被多个进程访问时遭到破坏。具体而言,闩锁在以下情况下保护数据结构:
- 被多个会话同时修改
- 正在被一个会话读取时,又被另一个会话修改
- 正在被访问时,其内存被释放(换出)
通常,一个单一的闩锁保护 SGA 中的多个对象。例如,后台进程(如 DBWn 和 LGWR)从共享池分配内存来创建数据结构。为分配此内存,这些进程使用共享池闩锁来串行化对内存的访问,以防止两个进程同时尝试检查或修改共享池。内存分配后,其他进程可能需要访问共享池区域,如用于解析所需的库高速缓存。在这种情况下,进程只在库缓存获取闩锁,而不是在整个共享池。
与行锁之类的入队闩锁不同,闩锁不允许会话排队。当闩锁可用时,请求闩锁的第一个会话将获得它的独占访问权限。闩锁旋转发生在当一个进程不断地循环来请求一个闩锁时,而闩锁睡眠发生在重新发起闩锁请求之前,释放 CPU 时。
通常,一个 Oracle 进程在操作或查看一种数据结构时,只需在一个极短的时间内获得闩锁。例如,仅仅为某一名员工处理工资更新,数据库就可能需要获取并释放成千上万个闩锁。闩锁的实现依赖于操作系统,特别是在一个进程是否会在闩锁上等待以及会在闩锁等待多长时间方面。
闩锁的增加意味着并发的降低。例如,过度硬解析操作会产生库缓存闩锁争用。V$LATCH 视图包含每个闩锁的详细使用情况的统计信息,包括每个闩锁被请求和被等待的次数。
互斥体
互斥对象(mutex)是一种底层机制,用于防止在内存中的对象在被多个并发进程访问时,被换出内存或遭到破坏。互斥体类似于闩锁,但闩锁通常保护一组对象,而互斥体通常保护单个对象。
互斥体提供以下几个优点:
- 互斥体可以减少发生争用的可能性。
由于闩锁保护多个对象,当多个进程试图同时访问这些对象的任何一个时,它可能成为一个瓶颈。而互斥体仅仅串行化对单个对象的访问,而不是一组对象,因此互斥体提高了可用性。 - 互斥体比闩锁消耗更少的内存。
- 在共享模式下,互斥体允许被多个会话并发引用。
内部锁
内部锁是比闩锁和互斥体更高级、更复杂的机制,并用于各种目的。
数据库使用以下类型的内部锁:
- 字典缓存锁
这些锁的持续时间很短,当字典缓存中的条目正在被修改或使用时被持有。他们保证正在被解析的语句不会看到不一致的对象定义。字典缓存锁可以是共享的或独占的。共享锁在解析完成后被释放,而独占锁在 DDL 操作完成时释放。 - 文件和日志管理锁
这些锁保护各种文件。例如,一种内部锁保护控制文件,以便一次只有一个进程可以对其进行更改。而另一种锁用于协调联机重做日志文件的使用和归档。数据文件被锁定,确保数据库被多个实例以共享模式装载,或以独占模式被单个实例装载。因为文件和日志锁表示文件的状态,这些锁必要时会被持有较长一段时间。 - 表空间和撤销段锁
这些锁保护的表空间和撤销段。例如,访问数据库的所有实例对一个表空间是否处于联机或脱机必须保持一致。撤销段被锁定,以便只能有一个数据库实例可以写入该段。
手动数据锁概述
您可以手动覆盖 Oracle 数据库的默认锁定机制。
Oracle 数据库自动执行锁定,以确保数据并发性、数据完整性、和语句级读取一致性。但是,覆盖默认锁定在以下情况下很有用:
- 应用程序需要事务级读取一致性或可重复读取。
在这种情况下,查询在整个事务持续期间必须产生一致的数据,但不反映其他事务所做的更改。通过使用显式锁定、只读事务、可串行化事务、或覆盖默认锁定,可以实现事务级别的读取一致性。 - 应用程序需要事务对资源具有独占访问权限,以便该事务不必等待其他事务完成就可以继续。
您可以在会话级或事务级覆盖数据库的自动锁定。在会话级,会话可以使用 ALTER SESSION 语句设置需要的事务隔离级别。在该事务级别,包括以下 SQL 语句的事务,会覆盖数据库的默认锁定:
- SET TRANSACTION ISOLATION LEVEL 语句
- LOCK TABLE 语句(锁定一张表,或与视图一起使用时则锁定其基表)
- SELECT … FOR UPDATE 语句
由上述语句获取的锁在事务结束后被释放,或回滚到保存点后释放。
如果在任何级别覆盖了 Oracle 数据库的默认锁定,则数据库管理员或应用程序开发人员应确保重写锁定过程能正常运行。锁定过程必须满足以下标准:可以保证数据完整性、可以接受的数据并发性、死锁不会发生(若发生,能被适当地处理)。
用户定义的锁的概述
使用 Oracle 数据库锁定管理服务,您可以为特定应用程序定义您自己的锁。
例如,您可以创建一个锁,来串行化到文件系统上的一个消息日志的访问。因为保留的用户锁与 Oracle 数据库锁一样,它具有包括死锁检测在内的 Oracle 数据库锁的所有功能。用户锁永远不会与数据库锁发生冲突,因为他们用前缀 UL 标识。
可通过 DBMS_LOCK 包中的过程来使用 Oracle 数据库的锁定管理服务。您可以在 PL/SQL 块中包含如下语句:
- 请求一个特定类型的锁
- 给锁起一个唯一的名称,以与(同一实例或不同实例中)另一个过程中的某个锁相区别
- 更改锁类型
- 释放锁




