TX/TM 锁机制详解与阻塞会话快速定位

在 Oracle 数据库的日常运维中,锁与阻塞问题是 DBA 最常面对的性能故障之一。一个未被及时处理的阻塞会话,可能在数分钟内引发连锁反应,导致整个业务系统的雪崩式瘫痪。本文将从 Oracle 锁机制的底层原理出发,系统讲解 TX 锁、TM 锁的工作机制,并提供一套完整的阻塞诊断与处理方案。

一、问题背景

在生产环境中,锁与阻塞是最常见也最棘手的性能问题之一。当一个会话持有某个资源的锁,而另一个会话需要获取同一资源的兼容锁时,后者就必须等待——这就是阻塞(Blocking)。与简单的慢 SQL 不同,阻塞问题往往具有突发性和传染性。

阻塞的雪崩效应是 DBA 最担心的场景:当一个核心事务表上的行被长时间锁定,所有依赖该表的业务操作都会排队等待。随着等待队列的拉长,数据库连接池被迅速耗尽,应用服务器开始报连接超时,最终导致整个业务链路瘫痪。在实际案例中,一个简单的 UPDATE 语句未及时 COMMIT,就可能在 5 分钟内导致上百个会话进入等待状态。

死锁(Deadlock)则是锁问题的另一种极端形式。当两个或多个会话相互等待对方释放资源时,Oracle 会自动检测死锁并通过回滚其中一个会话来打破循环等待。虽然 Oracle 能自动处理死锁,但频繁的死锁往往意味着应用层事务设计存在根本性缺陷。

因此,掌握 Oracle 的锁机制原理、具备快速定位和处理阻塞会话的能力,是每一位 DBA 的核心技能。

二、理论分析

2.1 Oracle 锁机制概览

Oracle 的锁机制可以分为以下几大类:

按功能分类:

锁类型 说明 典型场景
DML Lock 保护数据行和表结构不被并发修改 SELECT FOR UPDATE, UPDATE, DELETE
DDL Lock 保护数据字典对象定义 ALTER TABLE, DROP INDEX
Latch 内存结构的轻量级保护机制 Buffer Cache, Shared Pool
Mutex 比 Latch 更细粒度的内存保护 Cursor Pin, Library Cache

DML 锁的模式(Lock Mode):

Oracle 使用数字编码表示锁的级别,从低到高依次为:

  • **Row Share (RS, Mode=2)**:SELECT ... FOR UPDATE 获取,允许其他会话并发读写
  • **Row Exclusive (RX, Mode=3)**:INSERT/UPDATE/DELETE 获取,允许其他会话并发 DML
  • **Share (S, Mode=4)**:CREATE INDEX 获取,允许并发读但禁止写
  • **Share Row Exclusive (SRX, Mode=5)**:较少使用,介于 Share 和 Exclusive 之间
  • **Exclusive (X, Mode=6)**:最高级别,完全独占资源

锁的兼容性矩阵决定了并发行为:两个 Row Exclusive 锁可以共存(所以多个会话可以同时对同一张表做 DML),但 Exclusive 锁与任何其他锁都不兼容。

2.2 TX 锁 (Transaction Lock)

TX 锁是 Oracle 中最重要的锁类型之一,它与事务(Transaction)紧密关联。

TX 锁的获取时机:

当一个会话执行 DML 操作并修改数据时,Oracle 会在 Undo Segment 的事务表中分配一个事务槽(Slot),同时在被修改的数据行上标记事务信息。此时该会话就持有了一个 TX 锁。注意,TX 锁是在事务提交或回滚之前一直持有的,而不是在语句执行完就释放。

TX 锁的 Enqueue Type:

V$LOCK 视图中,TX 锁的 TYPE 列值为 'TX',而 REQUESTLMODE 列的组合揭示了等待的具体原因:

  • TX-4(Mode=4,ITL 等待):请求者需要在数据块的 ITL(Interested Transaction List)中分配一个事务槽,但所有可用的 ITL 槽都已被占用。这通常发生在高并发场景下,或者表的 INITRANS 参数设置过低时。

  • TX-6(Mode=6,行锁等待):请求者想要修改某一行,但该行已经被另一个活动事务锁定。这是最常见的 TX 等待类型,通常由长事务引起。

ITL 争用详解:

ITL(Interested Transaction List)位于每个数据块的块头区域,记录了哪些事务正在修改该块中的数据。每个 ITL 条目占 24 字节,默认情况下,一个数据块至少有 INITRANS 个 ITL 槽(默认值为 2),最多可以动态扩展到 MAXTRANS 个(11g 以后 MAXTRANS 固定为 255)。

当大量并发事务同时修改同一个数据块时,如果 ITL 槽不足,新的事务就必须等待——这就是 ITL 争用。在 V$LOCK 中表现为 TYPE='TX'REQUEST=4

2.3 TM 锁 (DML Enqueue)

TM 锁(也称为 DML Enqueue)用于保护表的结构不被 DML 操作期间的 DDL 操作修改。当一个会话对某张表执行 DML 操作时,Oracle 会在表级别获取一个 TM 锁。

TM 锁的模式与 DML 操作的关系:

  • INSERT/UPDATE/DELETE 获取 Row Exclusive (RX, Mode=3) 的 TM 锁
  • SELECT ... FOR UPDATE 获取 Row Share (RS, Mode=2) 的 TM 锁
  • LOCK TABLE ... IN SHARE MODE 获取 Share (S, Mode=4) 的 TM 锁

由于多个 RX 锁可以兼容共存,所以正常的并发 DML 不会因为 TM 锁产生阻塞。

外键缺失索引导致的 TM 锁升级:

这是一个经典的 Oracle 性能陷阱。当子表的外键列没有索引时,对父表执行 DELETEUPDATE 主键操作时,Oracle 必须对子表加 Share (S, Mode=4) 级别的 TM 锁来检查是否存在子记录。Share 锁与 Row Exclusive 锁不兼容,这意味着在锁持有期间,子表上的任何 DML 操作都会被阻塞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 检查外键列缺失索引的情况
SELECT
c.table_name AS child_table,
c.constraint_name AS fk_constraint,
cc.column_name AS fk_column
FROM
user_constraints p
JOIN user_constraints c ON c.r_constraint_name = p.constraint_name
JOIN user_cons_columns cc ON cc.constraint_name = c.constraint_name
WHERE
p.constraint_type = 'P'
AND c.constraint_type = 'R'
AND NOT EXISTS (
SELECT 1
FROM user_ind_columns i
WHERE i.table_name = cc.table_name
AND i.column_name = cc.column_name
AND i.column_position = cc.position
);

2.4 阻塞链分析

在复杂的生产环境中,阻塞往往不是简单的”一个会话阻塞另一个”,而是形成一条甚至多条阻塞链(Blocking Chain)。

锁等待链的形成:

1
Session A (持有锁) → 阻塞 → Session B (等待锁) → 阻塞 → Session C (等待锁)

Session B 等待 A 释放锁,而 Session C 又因为 B 持有的另一个资源而等待 B。这种链式反应在高并发系统中非常常见。

V$LOCK, V$SESSION, V$TRANSACTION 的关系:

  • V$LOCK:记录所有持有的锁和请求中的锁
  • V$SESSION:通过 LOCKWAIT 列关联到 V$LOCKKADDR
  • V$TRANSACTION:通过 V$SESSION.TADDR 关联,获取事务的详细信息(Undo 使用量等)

理解这三张视图的关系是诊断锁问题的关键。V$LOCKBLOCK=1 的记录表示该会话正在阻塞其他会话,而 REQUEST>0 的记录表示该会话正在等待获取锁。

三、实战操作

3.1 阻塞会话定位

以下是我在日常运维中使用的核心阻塞诊断脚本,已经过多生产环境验证:

脚本一:快速定位阻塞源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
-- 快速查看当前所有阻塞关系
SELECT
s1.sid AS blocking_sid,
s1.serial# AS blocking_serial,
s1.username AS blocking_user,
s1.machine AS blocking_machine,
s1.program AS blocking_program,
s1.status AS blocking_status,
s2.sid AS waiting_sid,
s2.serial# AS waiting_serial,
s2.username AS waiting_user,
s2.sql_id AS waiting_sql_id,
w.type AS lock_type,
w.id1 AS lock_id1,
w.id2 AS lock_id2,
w.ctime AS wait_seconds
FROM
v$lock w
JOIN v$session s2 ON s2.sid = w.sid
JOIN v$lock h ON h.type = w.type
AND h.id1 = w.id1
AND h.id2 = w.id2
AND h.lmode > 0
JOIN v$session s1 ON s1.sid = h.sid
WHERE
w.request > 0
AND h.lmode > 0
AND s1.sid != s2.sid
ORDER BY
w.ctime DESC;

脚本二:完整阻塞链分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
-- 递归查询完整阻塞链
WITH blocking_tree AS (
-- 锚点:找到所有阻塞源(没有被别人阻塞的会话)
SELECT
s.sid,
s.serial#,
s.username,
s.machine,
s.program,
s.sql_id,
s.status,
s.last_call_et,
t.used_ublk AS undo_blocks,
t.start_time,
CAST(s.sid AS VARCHAR2(1000)) AS path,
0 AS level
FROM
v$session s
JOIN v$lock l ON l.sid = s.sid AND l.type = 'TX' AND l.lmode > 0
LEFT JOIN v$transaction t ON s.taddr = t.addr
WHERE
s.sid IN (
SELECT h.sid
FROM v$lock h
WHERE h.request = 0 AND h.lmode > 0
AND h.type = 'TX'
)
AND s.sid NOT IN (
SELECT w.sid
FROM v$lock w
WHERE w.request > 0
)
UNION ALL
-- 递归:找到被当前层阻塞的会话
SELECT
s2.sid,
s2.serial#,
s2.username,
s2.machine,
s2.program,
s2.sql_id,
s2.status,
s2.last_call_et,
NULL,
NULL,
bt.path || ' -> ' || s2.sid,
bt.level + 1
FROM
blocking_tree bt
JOIN v$lock w ON w.request > 0 AND w.type = 'TX'
JOIN v$lock h ON h.type = w.type AND h.id1 = w.id1
AND h.id2 = w.id2 AND h.lmode > 0
AND h.sid = bt.sid
JOIN v$session s2 ON s2.sid = w.sid
)
SELECT
LPAD(' ', level * 2, ' ') || sid AS blocking_tree,
username,
machine,
status,
sql_id,
last_call_et AS idle_seconds,
undo_blocks,
path
FROM
blocking_tree
START WITH
level = 0
CONNECT BY
PRIOR sid = (
SELECT h.sid
FROM v$lock h
WHERE h.type = 'TX' AND h.lmode > 0
AND h.id1 = (
SELECT w2.id1 FROM v$lock w2
WHERE w2.sid = PRIOR sid AND w2.request > 0
)
AND ROWNUM = 1
)
ORDER SIBLINGS BY
last_call_et DESC;

脚本三:等待事件关联分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
-- 结合等待事件查看阻塞会话的详细信息
SELECT
s.sid,
s.serial#,
s.username,
s.sql_id,
s.event,
s.wait_class,
s.seconds_in_wait,
s.state,
sw.p1 AS p1_raw,
sw.p2 AS p2_raw,
sw.p3 AS p3_raw,
DECODE(sw.event,
'enq: TX - row lock contention',
'行锁等待 P1=' || TO_CHAR(sw.p1,'FM0XXXXXXX')
|| ' P2=' || sw.p2
|| ' P3=' || sw.p3,
'enq: TX - allocate ITL entry',
'ITL等待',
'enq: TM - contention',
'TM锁等待 Obj#=' || sw.p2,
sw.event
) AS lock_detail
FROM
v$session s
JOIN v$session_wait sw ON sw.sid = s.sid
WHERE
s.blocking_session IS NOT NULL
OR s.sid IN (
SELECT sid FROM v$lock WHERE request > 0
)
ORDER BY
s.seconds_in_wait DESC;

V$LOCK 视图关键列解读:

列名 说明
SID 持有或请求锁的会话 ID
TYPE 锁类型(TX, TM, UL 等)
ID1, ID2 锁资源标识符,TX 锁中 ID1=回滚段号,ID2=事务槽号
LMODE 持有的锁模式(0=无,2=RS,3=RX,4=S,5=SRX,6=X)
REQUEST 请求的锁模式(>0 表示正在等待)
CTIME 锁已持有或等待的时间(秒)
BLOCK 是否正在阻塞其他会话(1=是)

3.2 安全处理阻塞

定位到阻塞源后,需要根据具体情况决定处理策略。

Kill Session 的最佳实践:

在确认阻塞源后,优先联系应用端确认该事务是否可以回滚。如果需要 DBA 介入,使用以下命令:

1
2
3
4
5
-- 标准 Kill Session(等待事务回滚完成后释放)
ALTER SYSTEM KILL SESSION 'sid,serial#' ;

-- 强制 Kill Session(立即断开连接,异步回滚事务)
ALTER SYSTEM KILL SESSION 'sid,serial#' IMMEDIATE;

IMMEDIATE 选项的注意事项:

  • IMMEDIATE 并不意味着跳过回滚,它只是立即断开客户端连接,事务回滚在后台异步进行
  • 在 RAC 环境中需要指定 INST_IDALTER SYSTEM KILL SESSION 'sid,serial#,@inst_id' IMMEDIATE;
  • 被 Kill 的会话如果涉及大量 DML,回滚过程可能持续很长时间,在此期间相关资源仍被锁定

ORA-00031 处理:

如果 Kill Session 后会话仍然存在(状态为 KILLED),需要在操作系统层面清理:

1
2
3
4
5
6
7
-- 查找被 Kill 但仍存在的会话的 SPID
SELECT sid, serial#, status, spid, program
FROM v$session s JOIN v$process p ON s.paddr = p.addr
WHERE status = 'KILLED';

-- 在操作系统层面终止进程(Linux/Unix)
-- kill -9 <spid>

3.3 死锁处理

ORA-00060 错误分析:

当 Oracle 检测到死锁时,会在被选为牺牲者的会话上抛出 ORA-00060: deadlock detected while waiting for resource 错误,同时自动回滚该会话的当前语句。

Deadlock Trace 文件解读:

Oracle 在检测到死锁时会在 USER_DUMP_DEST(或 DIAGNOSTIC_DEST)目录下生成 Trace 文件。以下是一个典型的死锁 Trace 文件关键片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Deadlock graph:
---------Blocker(s)-------- ---------Waiter(s)---------
Resource Name process session holds waits process session holds waits
TX-000a001f-000002e8 32 143 X 35 208 X
TX-00090015-000002f0 35 208 X 32 143 X

session 143: DID 0001-0020-00000002 session 208: DID 0001-0023-00000001
session 208: DID 0001-0023-00000001 session 143: DID 0001-0020-00000002

Rows waited on:
Session 143: obj - rowid = 00012345 - AAAPoCAAEAAABbXAAA
Session 208: obj - rowid = 00012346 - AAAPoDAAEAAACcYBBB

----- Current SQL Statement for this session -----
UPDATE orders SET status = 'SHIPPED' WHERE order_id = 1001;

解读要点:

  1. Blocker/Waiter 矩阵:清楚展示了两个会话的相互等待关系。Session 143 持有 TX-…2e8 的 X 锁并等待 TX-…2f0 的 X 锁,而 Session 208 恰好相反。
  2. RowidRows waited on 部分指出了具体等待的数据行,可以通过 DBMS_ROWID 包定位到具体的表和数据块。
  3. SQL 语句:Trace 文件会记录发生死锁时正在执行的 SQL,这是分析根因的关键。

实战死锁分析案例:

某电商系统频繁报 ORA-00060,分析 Trace 文件发现如下模式:

  • Session A 执行:UPDATE parent_table SET ... WHERE id = 1(锁定父表行)
  • Session A 执行:UPDATE child_table SET ... WHERE parent_id = 2(尝试锁定子表行)
  • Session B 执行:UPDATE parent_table SET ... WHERE id = 2(锁定父表行)
  • Session B 执行:UPDATE child_table SET ... WHERE parent_id = 1(尝试锁定子表行)

两个会话交叉锁定不同资源,形成经典死锁。解决方案:在应用层统一对 parent_id 排序后再执行更新,确保所有事务按相同的顺序获取锁。

常见死锁模式与预防:

  1. 交叉更新死锁:不同事务以不同顺序更新相同资源集 → 统一加锁顺序
  2. 外键死锁:子表外键列缺失索引,父表删除与子表插入冲突 → 为外键列创建索引
  3. 位图索引死锁:位图索引的行锁粒度为数据块级别 → 高并发表避免使用位图索引
  4. ITL 争用死锁:INITRANS 设置过低导致事务无法获得 ITL 槽 → 调整 INITRANS

3.4 ITL 争用优化

INITRANS 参数调整:

V$LOCK 中频繁出现 TYPE='TX'REQUEST=4 的等待时,说明存在 ITL 争用。调整方法如下:

1
2
3
4
5
6
7
8
9
10
-- 查看当前表的 INITRANS 设置
SELECT table_name, ini_trans, max_trans
FROM user_tables
WHERE table_name = 'YOUR_TABLE';

-- 修改 INITRANS(需要重建表或在线重定义)
ALTER TABLE your_table INITRANS 10;

-- 对于已有的高并发表,建议通过 Online Rebuild 生效
ALTER INDEX your_idx REBUILD ONLINE INITRANS 20;

高并发表的 ITL 配置建议:

场景 INITRANS 建议 说明
普通业务表 2-5(默认 2) 一般场景无需调整
热点表(大量并发更新) 10-20 如订单表、库存表
频繁批处理表 10-15 批量更新同一数据块
高并发索引 10-25 B-Tree 索引根块和分支块热点

需要注意的是,INITRANS 只影响新分配的数据块。对于已有数据块,需要通过 ALTER TABLE ... MOVEALTER INDEX ... REBUILD 来重新组织。

四、结果验证

在处理完阻塞问题后,需要进行一系列验证来确保业务恢复正常。

阻塞解除确认:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-- 确认没有残留的阻塞关系
SELECT COUNT(*) AS blocking_count
FROM v$lock w
WHERE w.request > 0
AND EXISTS (
SELECT 1 FROM v$lock h
WHERE h.type = w.type
AND h.id1 = w.id1
AND h.id2 = w.id2
AND h.lmode > 0
AND h.sid != w.sid
);
-- 期望返回 0

-- 确认被影响的会话已经恢复
SELECT sid, serial#, status, event, seconds_in_wait
FROM v$session
WHERE event LIKE '%enq: TX%'
OR event LIKE '%enq: TM%';
-- 期望返回无结果或会话已正常执行

锁等待指标监控:

建立监控基线,持续跟踪以下指标:

1
2
3
4
5
6
7
8
9
10
11
-- 当前锁等待统计(定期采集)
SELECT
event,
COUNT(*) AS session_count,
MAX(seconds_in_wait) AS max_wait_seconds,
AVG(seconds_in_wait) AS avg_wait_seconds
FROM v$session
WHERE wait_class = 'Application'
AND event LIKE '%enq:%'
GROUP BY event
ORDER BY session_count DESC;

应用层事务优化验证:

  • 确认长事务已优化,事务持有时间缩短
  • 验证外键索引已补全
  • 检查 INITRANS 调整后 ITL 争用是否消除
  • 通过 AWR 报告对比调整前后的 enq: TX 等待事件

五、经验总结

锁问题的预防策略

  1. 监控先行:部署锁等待的实时监控告警,在阻塞演变为雪崩之前及时介入。建议设置阈值:单个阻塞链超过 5 个等待会话或阻塞时间超过 60 秒时触发告警。

  2. 外键索引全覆盖:这是一条铁律——所有外键列都必须有索引。可以在建表规范中强制要求,或定期运行前面提供的外键缺失索引检查脚本。

  3. 合理设置 INITRANS:对于已知的热点表和高并发索引,提前设置较高的 INITRANS 值,避免上线后的 ITL 争用问题。

  4. 避免长事务:事务持有时间越长,锁冲突的概率越大。应用设计时应遵循”短事务”原则——尽快提交或回滚。

应用层事务设计建议

  • 统一加锁顺序:当多个事务需要修改相同资源集时,确保按相同的顺序获取锁,从根本上避免死锁。
  • 合理使用隔离级别:不要盲目使用 SERIALIZABLE,它会显著增加锁冲突的概率。
  • 批量操作分段提交:大批量的 UPDATE/DELETE 操作应分批执行并定期 COMMIT,避免长时间持有锁。
  • 避免在事务中包含外部调用:HTTP 请求、文件 I/O 等外部操作不应放在数据库事务中,否则会将事务时间拉长数倍。

常见锁问题快速诊断清单

现象 可能原因 快速诊断方法
大量会话等待 enq: TX - row lock contention 长事务阻塞 V$LOCKBLOCK=1 会话
大量会话等待 enq: TX - allocate ITL entry ITL 争用 检查热点表的 INITRANS
大量会话等待 enq: TM - contention TM 锁升级 检查外键列缺失索引
频繁 ORA-00060 应用死锁 分析 Deadlock Trace 文件
Kill Session 后会话不消失 回滚未完成 OS 层面 kill SPID
V$TRANSACTION 中大量 Undo 使用 超长事务 USED_UBLKSTART_TIME

锁问题的处理看似复杂,但只要掌握了 V$LOCK / V$SESSION / V$TRANSACTION 三张核心视图的关系,配合本文提供的诊断脚本,绝大多数锁问题都能在几分钟内定位根因并完成处理。记住,预防永远优于治疗——合理的事务设计和完善的监控体系,才是避免锁问题影响业务的根本保障。