数据库基础体系 · 第 108/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。

Oracle 锁与隔离:行锁、ITL、读一致性、死锁和诊断

Oracle 的锁问题经常表现为“某条 SQL 卡住了”,但“卡住”可能来自完全不同的机制:

  • 一个事务修改了另一事务要修改的行;
  • 数据块没有足够的 ITL 槽位记录并发事务;
  • SELECT 正在通过 Undo 构造语句开始时的数据版本;
  • SELECT FOR UPDATE 必须访问当前版本并等待行锁;
  • 两个事务形成了循环等待,触发死锁检测;
  • DDL、DML、外键检查或表级锁之间发生了冲突。

要准确判断问题,必须把几个概念分开:

  1. 事务隔离决定一个语句或事务能看见哪些已提交版本;
  2. 读一致性负责构造符合 SCN 的数据视图;
  3. 行锁和表锁负责保护正在修改的数据;
  4. ITL记录数据块中哪些事务正在影响哪些行;
  5. 死锁检测根据等待关系发现循环;
  6. 诊断视图和 Trace帮助还原等待链、事务和 SQL。

一、先建立几个基础对象:事务、SCN、Undo 与当前版本

1. 事务不是一条 SQL

Oracle 中,事务通常从第一个需要事务语义的 SQL 开始,到以下操作之一结束:

COMMIT;
ROLLBACK;

事务也可能因会话断开而被回滚。某些 DDL 会隐式提交,例如执行 DDL 前后可能发生隐式提交,因此不能把 DDL 当作普通 DML 看待。

一个事务可以包含多条 SQL:

UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;
-- 事务仍未结束
COMMIT;

COMMIT 前,其他会话通常不能看到这两次修改;但它们可能在读取时通过 Undo 看到修改之前的已提交版本。

2. SCN 是读取时间点的逻辑标记

SCN,即 System Change Number,可以理解为 Oracle 用于排序数据库变更和判断可见性的逻辑时间点。

对一个普通查询而言,重要的是:

查询开始
  ↓
确定查询 SCN
  ↓
读取数据块
  ↓
如果数据块中的版本对该 SCN 不可见,则使用 Undo 构造旧版本

SCN 不是操作系统时间戳,也不是每条 SQL 都对应一个简单递增的“物理时钟”。在这里,只需要把它理解为 Oracle 判断版本是否已经对某个读取点可见的依据。

3. 当前版本与一致性版本

Oracle 访问数据时,需要区分两种模式:

  • 一致性读取(consistent read):按照某个 SCN 构造查询应该看到的版本;
  • 当前读取(current read):访问当前最新版本,通常用于修改或锁定。

普通的:

SELECT * FROM orders WHERE id = 10;

主要是为了返回某个一致性视图。

而:

SELECT * FROM orders
WHERE id = 10
FOR UPDATE;

不仅要返回数据,还要锁定当前行,因此不能简单返回一个已经过时的旧版本作为锁定对象。


二、Oracle 默认隔离级别:READ COMMITTED

Oracle 默认事务隔离级别是 READ COMMITTED。但它的含义不是“一个事务从头到尾只看到一个快照”。

1. READ COMMITTED 的核心规则

在 Oracle 中,READ COMMITTED 通常具有以下语义:

  • 不读取其他事务未提交的数据;
  • 每条查询在开始时确定自己的 SCN;
  • 同一事务中的两条普通查询,可能使用不同的 SCN;
  • 查询期间遇到其他事务已经提交的修改,可以看到符合本次查询 SCN 的版本;
  • 普通查询通常不会因为某行被另一个事务修改而等待行锁。

因此,READ COMMITTED语句级读一致性,而不是默认的事务级快照。

2. 非重复读示例

假设表中初始数据为:

CREATE TABLE demo_account (
    id      NUMBER PRIMARY KEY,
    balance NUMBER
);

INSERT INTO demo_account VALUES (1, 100);
COMMIT;

现在有两个会话。

会话 A

SELECT balance FROM demo_account WHERE id = 1;
-- 返回 100

会话 A 暂不提交。

会话 B

UPDATE demo_account
SET balance = 200
WHERE id = 1;

COMMIT;

会话 A 再次查询

SELECT balance FROM demo_account WHERE id = 1;
-- 返回 200

两次查询都在同一个事务中,但第二条查询开始时,B 的提交已经完成,因此第二条查询使用了更新后的 SCN。

这就是 READ COMMITTED 下可能出现的非重复读

3. 同一条长查询仍然保持语句级一致

如果 A 启动一条较长的查询:

SELECT ...
FROM large_table
WHERE ...;

Oracle 会为该查询确定一个查询 SCN。即使查询执行过程中其他会话提交了新数据,这条查询仍然尽量按照开始时的 SCN 返回结果,而不是前半段读取旧版本、后半段读取新版本。

这并不意味着整个事务保持相同视图。下一条 SQL 可以有新的 SCN。

4. READ COMMITTED 不允许脏读

所谓脏读,是指读取另一个事务尚未提交的数据。Oracle 的普通查询不会这样做。

例如:

会话 A

UPDATE demo_account
SET balance = 999
WHERE id = 1;
-- 未提交

会话 B

SELECT balance FROM demo_account WHERE id = 1;

B 不会读取到 999。Oracle 会根据查询 SCN,使用 Undo 构造 A 修改前的已提交版本,返回 100。

这也是 Oracle 中“读不阻塞写、写通常不阻塞普通读”的基础。


三、读一致性:为什么普通 SELECT 通常不等待行锁

1. Undo 保存的是构造旧版本所需的信息

假设某行原来是:

balance = 100

事务 A 执行:

UPDATE demo_account
SET balance = 200
WHERE id = 1;

A 尚未提交时,数据块中的当前行可能已经包含 200,同时 Undo 中保存了恢复到 100 所需的信息。

事务 B 此时执行普通查询。如果 B 的查询 SCN 早于 A 的提交 SCN,Oracle 会判断:

当前行版本:由 A 修改,且 A 对 B 的查询 SCN 不可见

然后沿着 Undo 找到前一个版本:

当前版本 200
    ↓ Undo
旧版本 100

B 返回 100,而不是等待 A 提交。

2. 读一致性的逐步判断

可以把一次普通查询抽象为:

1. 查询开始,确定 SCN = S
2. 读取数据块中的行版本 V
3. 判断 V 是否对 S 可见
4. 如果可见,直接返回 V
5. 如果不可见,通过 Undo 构造更早版本 V'
6. 重复判断,直到得到对 S 可见的版本

如果 Undo 已经被覆盖,Oracle 无法再构造所需版本,可能报:

ORA-01555: snapshot too old

这不是“锁等待超时”,而是读一致性所需的历史版本已经不可用

3. ORA-01555 与锁等待的区别

现象 主要原因 常见错误或等待
普通查询等待另一个事务 通常不是普通行锁本身 需要检查对象、块、DDL、并发访问路径
SELECT FOR UPDATE 等待 当前行被其他事务锁定 enq: TX - row lock contention
长查询无法构造旧版本 Undo 历史不足或被覆盖 ORA-01555: snapshot too old
ITL 槽位不足 数据块无法记录新的并发事务 enq: TX - allocate ITL entry

不要看到“查询很慢”就统一归因于锁。查询可能是在做大量一致性读取、等待 Undo、等待 I/O,也可能是在等待当前行锁。


四、Oracle 行锁:锁住的是什么,锁存在哪里

1. 行锁的逻辑含义

行锁表示:

事务 T 正在修改或锁定表中的某一行,
其他事务不能对同一行执行冲突的修改或锁定操作。

典型操作包括:

UPDATE t SET c = ... WHERE id = 1;
DELETE FROM t WHERE id = 1;
SELECT * FROM t WHERE id = 1 FOR UPDATE;

行锁通常在事务提交或回滚时释放,而不是在单条 DML 结束时释放。

这意味着:

UPDATE t SET c = ... WHERE id = 1;
-- 即使这条 UPDATE 已经返回,锁仍然存在

-- 其他事务仍可能等待
COMMIT;
-- 此时锁才释放

2. Oracle 没有一个独立的“每行锁对象表”

Oracle 的行锁信息主要通过数据块中的行信息和 ITL 关联到事务,而不是像某些系统那样为每一行都分配一个独立锁对象。

一行的行目录信息会关联到数据块中的 ITL 项。ITL 项记录:

  • 哪个事务正在使用该槽位;
  • 事务标识;
  • 事务状态相关信息。

因此,一个行锁的判断大致是:

行记录 → 行所在块中的 ITL 槽位 → 事务标识 → 事务状态

这解释了为什么诊断 Oracle 行锁时,经常看到的是 TX 事务锁,而不是“某行对应一个单独的锁对象”。

3. TX 与 TM

Oracle 锁诊断中最常见的两类锁是:

TX:事务相关锁

TX 通常代表事务级的锁资源。它可能用于:

  • 表示事务对自己修改的行拥有保护;
  • 让其他事务等待同一行的事务完成;
  • 处理 ITL 槽位分配不足等事务相关等待。

常见等待事件:

enq: TX - row lock contention
enq: TX - allocate ITL entry

TM:表级 DML 排他程度的锁

TM 是表级 DML enqueue,主要用于协调表级操作与 DML。

普通 DML 通常会对目标表持有允许并发 DML 的锁模式,而某些 DDL 需要更强的表级锁。它并不意味着“DML 把整张表锁住了”。

常见锁模式包括:

模式 名称 说明
2 Row-S / RS 行级共享语义
3 Row-X / RX 行级排他语义,常见于 DML
4 Share / S 共享
5 S/Row-X / SRX 共享行级排他
6 Exclusive / X 排他

锁模式之间是否冲突由 Oracle 的兼容矩阵决定。实际诊断时,应结合 V$LOCKLMODEREQUEST,而不是只看锁类型名称。

4. 行锁不会因为数量多而自动升级成表锁

Oracle 不采用常见的“行锁数量超过阈值后自动升级为表锁”的机制。一个事务可以锁定很多行,同时仍然保持行级并发语义。

但这不代表大量 DML 没有代价:

  • 每行修改都需要 Undo 和 Redo;
  • 数据块可能需要更多 ITL 槽位;
  • 索引维护成本增加;
  • 长事务会长时间持有锁;
  • 大量锁定可能扩大其他事务的等待范围。

“没有锁升级”不等于“大事务没有并发影响”。


五、ITL:数据块如何记录并发事务

1. ITL 是什么

ITL 是 Interested Transaction List,即“感兴趣事务列表”。

Oracle 数据块的块头中包含 ITL 区域。一个 ITL 槽位大致用于记录:

某个事务正在使用此数据块中的哪些行,
以及该事务对应的事务信息。

行记录不会把完整事务状态直接复制到每一行,而是通过行头中的信息关联到数据块内的 ITL 槽位。

因此,ITL 是:

  • 数据块级的事务登记结构;
  • 行锁实现的一部分;
  • 与数据块中的并发修改数量相关。

它不是一种额外的业务锁,也不是 Undo 段本身。

2. INITRANS 与 MAXTRANS

INITRANS 用于指定数据块创建时预留的初始 ITL 槽位数量。

例如:

CREATE TABLE hot_table (
    id NUMBER PRIMARY KEY,
    value NUMBER
)
INITRANS 8;

这意味着新分配的数据块会预留更多初始 ITL 槽位,以便并发事务更容易同时登记。

历史版本中还存在 MAXTRANS,用于限制 ITL 槽位数量上限。现代 Oracle 版本中,MAXTRANS 已不再是通常需要配置的有效并发控制手段,不能把它当作解决 ITL 问题的主要参数。

3. ITL 槽位不足如何发生

设一个数据块最初只有两个 ITL 槽位:

数据块 B:
  ITL 1 → 事务 T1
  ITL 2 → 事务 T2

现在事务 T3 也要修改该块中的另一行:

T3 修改块 B
  ↓
需要新的 ITL 槽位
  ↓
块中没有可复用的槽位
  ↓
如果块没有足够空间扩展 ITL,T3 等待

这时等待的并不是“同一行已经被 T1 锁住”。即使 T3 修改的是完全不同的行,也可能因为无法在数据块中登记自己而等待。

典型等待事件是:

enq: TX - allocate ITL entry

4. ITL 等待与行锁等待的区别

行锁等待

两个事务修改同一行:

T1:UPDATE t SET value = 2 WHERE id = 1;  -- 未提交
T2:UPDATE t SET value = 3 WHERE id = 1;  -- 等待 T1

典型表现:

enq: TX - row lock contention

ITL 等待

两个事务修改不同的行,但这些行位于同一数据块,且该块没有足够可用 ITL:

T1 修改块 B 的第 1 行
T2 修改块 B 的第 2 行
T3 修改块 B 的第 3 行,但块的 ITL 已耗尽

典型表现:

enq: TX - allocate ITL entry

二者都可能显示为 TX,但根因不同。仅看到 TX 不能直接得出“发生了同一行冲突”。

5. 为什么 PCTFREE 有时与 ITL 有关

ITL 扩展需要数据块中有可用空间。如果块被填得非常满,Oracle 可能没有足够空间增加 ITL 槽位。

PCTFREE 的直接目的通常是为将来行更新预留空间,但预留空间也可能让数据块更容易容纳额外的块头结构或行增长。它不是“ITL 开关”,也不是所有 ITL 问题都应通过提高 PCTFREE 解决。

需要结合以下因素判断:

  • 表或索引的 INITRANS
  • 块中的行和索引项分布;
  • 并发事务数;
  • 数据块是否高度集中;
  • 是否确实出现 allocate ITL entry 等等待;
  • 修改对象是表还是索引。

盲目调大 INITRANS 会增加每个块的开销,不能替代对热点块和事务边界的分析。


六、锁的生命周期:什么时候获得,什么时候释放

1. DML 获取锁

执行以下语句时,Oracle 通常需要获取相应的行级和事务级保护:

UPDATE orders
SET status = 'PAID'
WHERE order_id = 1001;

执行过程可以概括为:

1. 根据执行计划找到目标行
2. 访问目标数据块
3. 在块中登记或复用本事务的 ITL
4. 修改行和相关索引项
5. 记录 Undo 与 Redo
6. 保持事务锁直到 COMMIT 或 ROLLBACK

其中第 3 步可能遇到 ITL 不足,第 4 步如果目标行已经被其他事务修改,则可能等待行锁。

2. 行锁通常在事务结束时释放

UPDATE orders
SET status = 'PAID'
WHERE order_id = 1001;

-- 这里锁仍在
SELECT ... FROM ...;

COMMIT;
-- 锁释放

因此,以下代码结构容易制造长锁:

开始事务
  修改一行
  调用远程 HTTP 服务
  等待用户输入
  执行复杂查询
提交

问题不在于 Oracle 不能持有锁,而在于锁的持有时间被外部操作拉长。

3. ROLLBACK TO SAVEPOINT 不等于完整回滚

保存点只回滚保存点之后的修改:

SAVEPOINT p1;

UPDATE t SET value = 2 WHERE id = 1;

ROLLBACK TO p1;

这会撤销保存点之后的修改,但事务本身仍然存在。其他早于保存点获得的锁不一定全部释放,不能把保存点当成“结束事务”。


七、SELECT FOR UPDATE:读取与锁定同时发生

1. 普通 SELECT 与 SELECT FOR UPDATE 的区别

普通查询:

SELECT *
FROM orders
WHERE order_id = 1001;

目标是返回一致性数据,通常不锁定行。

锁定查询:

SELECT *
FROM orders
WHERE order_id = 1001
FOR UPDATE;

目标是:

  1. 找到符合条件的当前行;
  2. 获取该行的锁;
  3. 在事务结束前保持锁;
  4. 返回行数据。

因此,FOR UPDATE 可能等待其他事务提交。

2. 示例

会话 A

SELECT *
FROM orders
WHERE order_id = 1001
FOR UPDATE;

A 获得该行锁,但还没有提交。

会话 B

SELECT *
FROM orders
WHERE order_id = 1001
FOR UPDATE;

B 会等待 A。

如果 B 使用:

SELECT *
FROM orders
WHERE order_id = 1001
FOR UPDATE NOWAIT;

则不等待,无法立即获得锁时会报错,常见错误为:

ORA-00054: resource busy and acquire with NOWAIT specified or timeout expired

如果使用:

SELECT *
FROM orders
WHERE order_id = 1001
FOR UPDATE WAIT 5;

Oracle 最多等待指定时间。

3. SKIP LOCKED 的语义

在适合的锁定查询中可以使用:

SELECT order_id
FROM job_queue
WHERE status = 'READY'
FOR UPDATE SKIP LOCKED;

遇到已被其他事务锁定的行时跳过,而不是等待。

这适合某些并行消费队列的场景,但它改变了查询结果的语义:

  • 返回的是“当前未被锁定的可见行”;
  • 不保证返回完整的符合条件集合;
  • 两个消费者可能拿到不同批次;
  • 业务必须能够接受跳过和稍后重试。

SKIP LOCKED 不是通用的“消除锁问题”选项。


八、事务隔离级别:READ COMMITTED、SERIALIZABLE 和 READ ONLY

1. READ COMMITTED

Oracle 默认的 READ COMMITTED 以语句为单位建立读一致性。

它防止:

  • 脏读。

它允许:

  • 非重复读;
  • 某些场景下的幻读;
  • 同一事务不同语句看到不同提交状态。

2. SERIALIZABLE

可以通过以下方式设置事务隔离级别:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

该语句必须在事务开始时使用,不能在事务已有 DML 后随意切换。

Oracle 的 SERIALIZABLE 事务通常按照事务开始时的逻辑视图读取数据。若事务尝试修改一个在该事务开始后已被其他事务修改并提交的行,可能报:

ORA-08177: can't serialize access for this transaction

示例:

会话 A

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

SELECT value
FROM demo_account
WHERE id = 1;
-- 事务视图中看到 value = 100

会话 B

UPDATE demo_account
SET value = 200
WHERE id = 1;

COMMIT;

会话 A 再修改同一行

UPDATE demo_account
SET value = 300
WHERE id = 1;

A 可能收到 ORA-08177,因为该行在 A 的事务开始后已经被 B 修改并提交。

应用收到该错误后,通常需要回滚当前事务,并以新的事务重新读取和执行业务逻辑。

3. SERIALIZABLE 不是“所有范围都自动加锁”

Oracle 的 SERIALIZABLE 不等于对所有查询谓词自动建立数据库范围锁。尤其是:

SELECT COUNT(*)
FROM inventory
WHERE quantity < 10;

并不会因为使用了 SERIALIZABLE 就自动锁住“所有未来满足 quantity < 10 的行”这一逻辑范围。

如果业务不变量依赖一个范围条件,例如:

同一房间在同一时间不能有两个有效预约

仅仅读取“当前没有冲突预约”并随后插入,未必足够。应使用适合的唯一约束、显式锁定父对象或其他可验证的并发控制设计。

4. READ ONLY

可以设置只读事务:

SET TRANSACTION READ ONLY;

只读事务不能执行 DML。它适用于需要在一个一致视图中完成多次查询的场景。

它与普通 READ COMMITTED 的重要区别是:普通 READ COMMITTED 的每条语句可以有自己的 SCN,而只读事务提供事务级的一致读取语义。


九、锁与读一致性的边界:哪些读取会等待

1. 普通 SELECT 通常不等待未提交行修改

如前所述,Oracle 可以通过 Undo 返回旧版本:

T1 未提交修改行 R
T2 普通 SELECT
  → 读取 R 的当前块内容
  → 发现 T1 版本对 T2 的 SCN 不可见
  → 使用 Undo
  → 返回旧版本

所以“写阻塞读”不是 Oracle 普通查询的典型行为。

2. 当前读取必须等待

以下操作需要当前版本或锁定语义:

  • UPDATE
  • DELETE
  • SELECT FOR UPDATE
  • 某些涉及当前状态判断的内部操作。

因此:

普通 SELECT:可以读取旧版本
SELECT FOR UPDATE:必须锁定当前版本
UPDATE:必须修改当前版本

这就是下面两个语句行为不同的根本原因:

SELECT * FROM t WHERE id = 1;

与:

SELECT * FROM t WHERE id = 1 FOR UPDATE;

前者侧重“在某个 SCN 上读取”,后者侧重“拿到当前行并保护它”。


十、死锁:从等待到循环

1. 什么是死锁

死锁不是普通的单向等待,而是等待关系形成闭环。

设:

T1 等待 T2 持有的资源
T2 等待 T1 持有的资源

可以写成等待图:

T1 ─────等待─────> T2
T2 ─────等待─────> T1

不存在一个事务能够自行推进,因此必须中止其中一个事务或其中一条语句。

2. 两行更新顺序相反的经典死锁

准备数据:

CREATE TABLE lock_demo (
    id    NUMBER PRIMARY KEY,
    value NUMBER
);

INSERT INTO lock_demo VALUES (1, 10);
INSERT INTO lock_demo VALUES (2, 20);
COMMIT;

会话 A

UPDATE lock_demo
SET value = value + 1
WHERE id = 1;

A 锁住 ID 为 1 的行,但不提交。

会话 B

UPDATE lock_demo
SET value = value + 1
WHERE id = 2;

B 锁住 ID 为 2 的行,但不提交。

会话 A 再执行

UPDATE lock_demo
SET value = value + 1
WHERE id = 2;

A 等待 B。

会话 B 再执行

UPDATE lock_demo
SET value = value + 1
WHERE id = 1;

B 等待 A。

等待图变成:

A → B → A

Oracle 检测到死锁后,通常返回:

ORA-00060: deadlock detected while waiting for resource

并回滚导致死锁的语句。应用不能简单假定“整个事务已经被 Oracle 自动回滚”;发生错误后应根据事务边界明确执行 ROLLBACK 或重新开始事务。

3. 固定访问顺序可以破坏循环

如果所有事务都按主键升序访问:

UPDATE lock_demo
SET value = value + 1
WHERE id = 1;

UPDATE lock_demo
SET value = value + 1
WHERE id = 2;

那么两个事务可能发生等待,但通常不会形成“一个先拿 1、另一个先拿 2”的循环。

关键不是“减少锁”,而是让资源获取顺序满足全局顺序:

所有事务都先锁资源 1,再锁资源 2

这是消除这类死锁的因果机制。

4. 死锁不只来自显式更新顺序

真实系统中的死锁还可能来自:

  • 不同业务入口更新同一组表,但顺序不同;
  • 触发器隐式修改其他表;
  • 外键检查;
  • 删除父表记录与修改子表记录并发;
  • 索引块和表块的访问顺序差异;
  • DDL 与 DML 互相等待;
  • 分布式事务或跨实例访问;
  • 应用在持锁事务中再次调用会访问相同资源的代码。

外键索引的边界

在父表被删除或更新键值时,Oracle 需要检查子表中是否存在引用。子表外键没有索引时,相关操作可能产生更广泛的表级协调和并发影响,尤其在高并发父表 DML 中更明显。

但“所有外键都必须建立索引”不是脱离访问模式的绝对规则。应分析:

  • 父表是否频繁删除或更新被引用键;
  • 子表是否频繁按外键查询;
  • 并发 DML 是否集中;
  • 索引维护成本是否可接受。

十一、死锁、锁等待与 ITL 等待的区分

类型 等待对象 典型现象 是否一定是死锁
同一行锁冲突 其他事务持有的行 TX - row lock contention
ITL 不足 数据块中的 ITL 槽位 TX - allocate ITL entry
表级 DDL/DML 冲突 TM 或 DDL 锁 enq: TM - contention
循环等待 多个资源和多个事务 ORA-00060
NOWAIT 失败 资源当前被占用 ORA-00054
Undo 历史不足 一致性读所需旧版本 ORA-01555

最常见的误判是:

看到 TX → 认为一定是同一行锁
看到查询慢 → 认为一定是锁
看到一个等待会话 → 认为一定是死锁

这些结论都不充分。


十二、线上诊断:先看等待者,再找阻塞者

1. 查看当前会话的阻塞关系

在单实例或 RAC 环境中,可以先查询 GV$SESSION

SELECT
    s.inst_id,
    s.sid,
    s.serial#,
    s.username,
    s.status,
    s.event,
    s.wait_class,
    s.seconds_in_wait,
    s.blocking_instance,
    s.blocking_session,
    s.blocking_session_status,
    s.final_blocking_instance,
    s.final_blocking_session,
    s.sql_id,
    s.prev_sql_id
FROM gv$session s
WHERE s.state = 'WAITING'
  AND s.wait_class <> 'Idle';

重点关注:

  • EVENT:等待事件;
  • BLOCKING_SESSION:直接阻塞者;
  • FINAL_BLOCKING_SESSION:最终阻塞者;
  • SQL_ID:当前等待 SQL;
  • PREV_SQL_ID:产生事务或持锁操作的前一条 SQL。

需要注意,当前 SQL 不一定是持有锁的 SQL。一个会话可能已经执行完 UPDATE,正在等待用户操作或执行另一条查询,但它仍然持有前一条 DML 的锁。

2. 用 V$LOCK 配对等待者和阻塞者

V$LOCK 中常用字段包括:

  • TYPE:锁类型,例如 TXTM
  • ID1ID2:锁资源标识;
  • LMODE:当前持有的锁模式;
  • REQUEST:正在请求的锁模式;
  • BLOCK:是否正在阻塞其他会话。

可以使用类似查询查看资源上的等待与持有:

SELECT
    w.inst_id       AS waiter_inst,
    ws.sid          AS waiter_sid,
    ws.serial#      AS waiter_serial,
    ws.username     AS waiter_user,
    ws.event        AS waiter_event,
    b.inst_id       AS blocker_inst,
    bs.sid          AS blocker_sid,
    bs.serial#      AS blocker_serial,
    bs.username     AS blocker_user,
    w.type,
    w.id1,
    w.id2,
    w.request       AS waiter_request_mode,
    b.lmode         AS blocker_lock_mode
FROM gv$lock w
JOIN gv$session ws
  ON ws.inst_id = w.inst_id
 AND ws.sid = w.sid
JOIN gv$lock b
  ON b.type = w.type
 AND b.id1 = w.id1
 AND b.id2 = w.id2
 AND b.request = 0
 AND b.lmode > 0
JOIN gv$session bs
  ON bs.inst_id = b.inst_id
 AND bs.sid = b.sid
WHERE w.request > 0;

这个查询展示的是资源相同的锁请求与持有关系。在 RAC 中,锁可能由其他实例上的会话持有,因此不能只用本地 V$SESSION 视角判断。

3. 查看表级锁对象

如果要查看哪些会话对哪些数据库对象持有表级 DML 锁,可以查询:

SELECT
    lo.inst_id,
    lo.session_id AS sid,
    lo.oracle_username,
    lo.os_user_name,
    lo.locked_mode,
    o.owner,
    o.object_name,
    o.object_type
FROM gv$locked_object lo
JOIN dba_objects o
  ON o.object_id = lo.object_id
ORDER BY
    lo.inst_id,
    lo.session_id,
    o.owner,
    o.object_name;

LOCKED_MODE 需要结合 Oracle 锁模式解释。它只能说明对象级锁信息,不能直接告诉你某一行被谁锁住。

4. 关联事务

可以通过 GV$TRANSACTION 查看事务信息:

SELECT
    t.inst_id,
    t.addr,
    t.start_time,
    t.status,
    t.used_ublk,
    t.used_urec,
    t.xidusn,
    t.xidslot,
    t.xidsqn,
    s.sid,
    s.serial#,
    s.username,
    s.sql_id,
    s.prev_sql_id
FROM gv$transaction t
JOIN gv$session s
  ON s.inst_id = t.inst_id
 AND s.taddr = t.addr;

重要字段包括:

  • START_TIME:事务开始时间;
  • USED_UBLK:使用的 Undo 块数量;
  • USED_UREC:使用的 Undo 记录数量;
  • TADDR:会话当前事务地址。

长时间运行的事务通常值得优先调查,因为它可能同时造成:

  • 行锁长期不释放;
  • Undo 保留压力;
  • 其他会话排队;
  • 一致性读取成本增加。

5. 查看 SQL 文本

通过 SQL_ID 获取 SQL:

SELECT
    sql_id,
    child_number,
    parsing_schema_name,
    executions,
    sql_text
FROM v$sql
WHERE sql_id IN (:sql_id_1, :sql_id_2);

实际诊断时需要同时看:

  • 当前等待 SQL;
  • 上一条 SQL;
  • 事务开始后执行过的修改语句;
  • 应用请求或线程上下文。

只看当前 SQL_ID 很容易漏掉真正持锁的语句。


十三、Oracle 死锁 Trace 应该看什么

当 Oracle 检测到死锁时,通常会在诊断 Trace 中记录死锁图。不同版本和环境的格式可能不同,但核心信息通常包括:

  • 参与死锁的会话;
  • 每个会话持有的资源;
  • 每个会话正在请求的资源;
  • 会话对应的 SQL;
  • 锁类型和资源标识;
  • 哪个资源形成了循环。

分析死锁图时,应按以下顺序还原:

1. 找到每个会话正在等待什么
2. 找到另一个会话持有什么
3. 把资源映射回表、索引、事务或数据块
4. 查看两个会话各自先执行了什么
5. 按时间顺序还原锁获取顺序
6. 判断是否存在固定顺序、触发器、外键或 DDL 参与

死锁 Trace 是事后证据;GV$SESSIONGV$LOCK 更适合观察当前等待。

如果使用 ASH、AWR 等历史性能数据,还必须考虑 Oracle 对相关诊断功能的许可边界。是否可以在特定环境使用这些功能,不应只根据数据库中是否存在对应视图来判断。


十四、阻塞会话的处理与风险

1. 首先确认阻塞者是否仍然需要提交

确认以下事实:

  • 阻塞事务属于哪个应用用户;
  • 事务开始时间;
  • 持锁 SQL 或上一条 SQL;
  • 是否正在执行正常的大批量任务;
  • 是否处于空闲但未提交状态;
  • 是否是连接池遗留会话;
  • 是否存在最终阻塞者。

很多“数据库卡死”实际是应用执行了:

UPDATE ...

然后没有及时提交,甚至连接返回连接池后事务仍未结束。

2. 终止会话不是立即释放资源的魔法

管理员可能使用:

ALTER SYSTEM KILL SESSION 'sid,serial#' IMMEDIATE;

RAC 中还需要考虑实例标识,具体语法应以目标版本文档为准。

终止会话的风险包括:

  • 未提交事务需要回滚;
  • 回滚可能持续较长时间;
  • 其他会话在回滚完成前仍可能受到影响;
  • 应用请求可能失败;
  • 连接池可能重复使用或重建连接;
  • 如果误杀正常批处理,会造成更大业务影响。

因此,KILL SESSION 是故障处置手段,不是锁设计的替代品。执行后还要观察事务回滚和等待是否真正消失。


十五、一个完整的锁等待实验

下面的实验可以用于确认“锁在事务提交前保持”。

1. 建表

CREATE TABLE lock_test (
    id    NUMBER PRIMARY KEY,
    value NUMBER
);

INSERT INTO lock_test VALUES (1, 100);
COMMIT;

2. 会话 A 获取行锁

UPDATE lock_test
SET value = 200
WHERE id = 1;

不要执行 COMMIT

3. 会话 B 执行普通读取

SELECT value
FROM lock_test
WHERE id = 1;

预期:

返回 100

原因是 A 尚未提交,B 的普通查询不能看到 A 的版本,于是通过 Undo 读取旧版本。

4. 会话 B 执行锁定读取

SELECT value
FROM lock_test
WHERE id = 1
FOR UPDATE;

预期:

等待会话 A

原因是 B 不只是想读取旧版本,而是必须锁定当前行。

5. 会话 A 回滚

ROLLBACK;

会话 B 的 FOR UPDATE 随后可以继续,并获得可锁定的行。

这个实验直接展示了:

普通读取依赖读一致性
锁定读取依赖当前版本和行锁

十六、常见误解与反例

误解一:Oracle 读不阻塞写,所以所有 SELECT 都不会等待

反例:

SELECT ... FOR UPDATE;

这不是普通一致性读取,而是锁定当前行,可能等待。

此外,DDL、表级协调、I/O、Undo 和资源争用也可能让普通查询等待。

误解二:看到 TX 就说明两个事务修改了同一行

反例:

TX - allocate ITL entry

这可能是数据块没有可用 ITL 槽位,而不是目标行相同。

必须结合等待事件、对象、数据块和事务关系判断。

误解三:提交后锁一定在语句返回时已经释放

通常提交会使事务锁进入可释放状态,但高并发环境中还可能存在块访问、事务清理或其他后续资源处理。诊断时不应只根据客户端是否收到提交成功就推断所有等待会立即消失。

误解四:SERIALIZABLE 自动锁住查询范围

反例:

SELECT *
FROM reservations
WHERE room_id = 10
  AND start_time < :end_time
  AND end_time > :start_time;

随后另一个事务插入一条重叠预约。Oracle 不会因为这个谓词自动建立传统意义上的范围锁。业务不变量需要约束、显式锁定或可重试的并发设计。

误解五:回滚一条 SQL 就释放事务全部锁

反例:

SAVEPOINT p;

UPDATE t SET value = 2 WHERE id = 1;

ROLLBACK TO p;

保存点回滚的是部分修改,不等于结束整个事务。事务仍然可能持有其他资源。

误解六:杀掉阻塞会话后问题立刻消失

反例:

阻塞会话被终止
  ↓
未提交事务开始回滚
  ↓
回滚期间资源仍可能被清理或访问
  ↓
等待不会瞬间全部消失

应继续观察事务和等待状态。


十七、从机制推导工程设计

1. 事务边界决定锁持有时间

锁持有时间可以粗略表示为:

锁持有时间
≈ 从第一次修改或锁定开始
  到 COMMIT / ROLLBACK 完成

因此,以下因素会延长锁:

  • 网络调用;
  • 用户交互;
  • 大量无关查询;
  • 批处理单次事务过大;
  • 异常路径没有回滚;
  • 连接池中的连接被复用但事务未结束。

事务边界不是单纯的 API 风格问题,而是锁生命周期的一部分。

2. 统一资源顺序降低死锁概率

对于需要同时修改多个资源的业务,可以定义全局顺序:

账户按 account_id 升序
订单按 order_id 升序
表按固定表顺序

事务始终按相同顺序获取资源,就能破坏大量经典循环等待。

3. 用数据库约束表达真正的不变量

如果规则是:

一个用户只能有一个有效主地址

优先考虑唯一约束或函数索引,而不是:

SELECT COUNT(*) ...
如果为 0 再 INSERT ...

后者在并发下是典型的检查与插入竞态。

如果规则确实需要串行化某个业务对象,可以显式锁定稳定的父对象:

SELECT user_id
FROM users
WHERE user_id = :user_id
FOR UPDATE;

然后在持有该用户行锁的事务中检查并修改相关数据。这样锁定的是一个稳定的协调对象,而不是试图锁住一个可能尚不存在的范围。

4. 批量任务需要平衡事务大小

事务太小:

  • 提交次数多;
  • 日志和网络交互增加;
  • 总吞吐可能下降。

事务太大:

  • 锁持有时间长;
  • Undo 增长;
  • 失败回滚成本高;
  • 阻塞其他业务;
  • 更容易造成长查询的历史版本压力。

合适的批量大小取决于数据量、业务容忍度、Undo、Redo、并发和恢复要求,不能套用固定数字。

5. ITL 问题应针对热点块处理

确认确实存在 ITL 等待后,再考虑:

  • 提高对象的 INITRANS
  • 为未来块分配保留适当空间;
  • 减少大量并发事务集中更新同一数据块;
  • 调整数据分布或访问模式;
  • 区分表块热点与索引块热点。

如果实际等待是 TX - row lock contention,调 ITL 不会解决两个事务修改同一行的问题。


十八、把一次锁问题完整还原

一个可靠的分析过程通常是:

1. 识别等待事件
2. 找到等待者
3. 找到直接阻塞者和最终阻塞者
4. 查看阻塞事务开始时间
5. 查看当前 SQL 与上一条 SQL
6. 判断资源是 TX、TM、ITL 还是其他类型
7. 映射对象、数据块、索引或外键关系
8. 还原各事务获取资源的顺序
9. 判断是单向阻塞、ITL 等待还是循环等待
10. 评估提交、回滚或终止会话的影响
11. 修复事务边界、资源顺序、约束或数据块热点

其中最关键的判断是:

这是“等待一个事务结束”,
还是“等待数据块登记 ITL”,
还是“多个事务已经形成循环”?

三者都可能出现在锁相关告警中,但解决方法完全不同。


Oracle 的并发控制不是“给每一行加锁”这么简单。行锁通过数据块中的 ITL 和事务信息实现;普通查询通过 SCN 与 Undo 构造读一致性;SELECT FOR UPDATE 和 DML 则必须访问当前版本并获取保护;隔离级别决定可见性范围,但不会自动替业务建立所有谓词范围锁;死锁则是等待图中的循环,需要从资源和执行顺序还原。

掌握这些层次后,TXTM、ITL、ORA-01555ORA-00060ORA-08177 就不再是孤立的错误名,而是可以对应到具体状态变化、数据流和故障路径的诊断线索。


系列导航与关联阅读

官方资料

本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。