会话卡住、业务超时、CPU 不高但连接数上涨,大概率是阻塞,不是性能问题。第一反应去 kill 会话,往往杀错人:被阻塞的会话不持有锁,杀了它没用;真正持锁的会话一杀,可能回滚几十分钟。
定位阻塞只需要三步:找到谁在等、谁在挡、等的是什么锁。TX 是行级事务锁,TM 是表级锁,两者加锁顺序固定,先 TM 后 TX。把这两类分清,再看 DDL 和 latch,绝大多数现场都能解释。
本文所有实验都在同一个测试表上完成,会话号、SQL 和输出保持原样,可以直接照着复现。
一、锁从哪来
单用户环境数据访问是串行的,多用户并发读写同一份数据时,数据库必须同时保证并发性和一致性。Oracle 用多版本并发控制加锁机制协调并发,让每个事务在逻辑一致的状态下读和写。
按模式分,锁只有两类:
- 排它锁(X):对象被加 X 锁后,在 commit 或 rollback 释放之前,不能再加任何类型的锁。
- 共享锁(S):对象被加 S 锁后,还可以继续加 S 锁,但不能加 X 锁。
按对象分,是 DML 锁、DDL 锁和内存锁(latch)。DML 锁又分 TX 行锁和 TM 表锁。理解这三层的区别,比背等待事件名字有用。
二、TX 与 TM
2.1 TX 是事务锁,不是行数
执行 update、insert、delete、merge 或 select ... for update 时,Oracle 会对涉及的行加 TX 锁,直到事务提交或回滚。TX 锁是行级的,但一个 TX 锁可以覆盖多行,行锁标记记录在数据块上,不在内存的锁列表里逐行维护。
这就是为什么更新一百万行,v$lock 里通常仍然只有一个 TX 条目:它代表事务,不代表团里的每一行。
2.2 TM 是表级意向锁
TM 锁加在表对象上,作用是防止别人在事务未提交时改表结构,比如 truncate、drop、alter table。手工执行 lock table ... in share mode 也会加 TM 锁。
TM 有五个级别,对应 v$lock.lmode:
| LMODE | 名称 | 典型来源 |
|---|---|---|
| 2 | RS,row share | lock table ... in row share mode、部分在线 DDL |
| 3 | RX,row exclusive | insert、update、delete、merge |
| 4 | S,share | lock table ... in share mode、部分在线 DDL |
| 5 | SRX,share row exclusive | lock table ... in share row exclusive mode |
| 6 | X,exclusive | truncate、drop、多数 alter table |
DML 的顺序是先加 TM,再加 TX。普通 DML 在表上加 RX(3),然后在行上加 TX(6)。因为 RX 与 RX 兼容,多个会话可以同时 DML 同一张表,只要不碰同一行,TX 不冲突。
select ... for update 的表级锁模式在不同资料里有 RS 和 RX 两种说法。本文第三节的实验输出是 TM 3(RX),以现场 v$lock 为准;判断冲突时不要只看模式编号,要结合 request 和 block。
这一点很关键:看到两个会话都有 TM 3,不代表互相阻塞。真正决定行级冲突的是 TX。
2.3 兼容关系
Oracle 的 TM 锁兼容矩阵如下,行是已持有的锁,列是请求的锁,✓ 表示可以同时持有:
| 已持有 \ 请求 | RS(2) | RX(3) | S(4) | SRX(5) | X(6) |
|---|---|---|---|---|---|
| RS(2) | ✓ | ✓ | ✓ | ✓ | ✗ |
| RX(3) | ✓ | ✓ | ✗ | ✗ | ✗ |
| S(4) | ✓ | ✗ | ✓ | ✗ | ✗ |
| SRX(5) | ✓ | ✗ | ✗ | ✗ | ✗ |
| X(6) | ✗ | ✗ | ✗ | ✗ | ✗ |
从表里能直接读出三个结论。第一,DML 之间(RX 与 RX)永远兼容,所以 DML 不会被 DML 的表锁挡住。第二,S 与 RX 冲突,所以在表上建索引、加 S 锁期间,所有 DML 都要等。第三,X 与所有锁冲突,所以 truncate、drop 遇到任何未提交事务都会失败或等待。
三、先看一个最小的 TX 阻塞
会话 1 锁住一行:
set linesize 200
col name for a10
col remark for a20
select * from hruser.test_data where id=1 for update;
ID NAME REMARK CREATE_DT
---- ---------- ---------- --------------------
1 USER_1 测试数据1 2026/08/12 16:21:15
查自己的锁:
select type,lmode from v$lock where sid = (select sid from v$mystat where rownum <=1);
TY LMODE
-- ----------
TM 3
TX 6
TM 3 是 RX,TX 6 是排它,符合「先 TM 后 TX」的顺序。
此时会话 2 执行 DDL:
SQL> drop table hruser.test_data;
drop table hruser.test_data
*
ERROR at line 1:
ORA-00054: resource busy and acquire with NOWAIT specified or timeout expired
DDL 需要 X 级别的 TM 锁,与已持有的 RX 冲突,且 DDL 默认不等待,直接报 ORA-00054。
会话 3 执行更新:
set linesize 200
col name for a10
col remark for a20
select id,name,remark,create_dt from hruser.test_data order by id;
update hruser.test_data set id =2 where id=1;
会话 3 开始等待。注意 select 本身不会被阻塞,它读到的是 undo 里的一致性版本。
3.1 一个会话可以有多个 TM,但通常只有一个 TX
SQL> update hruser.test_data set id =2 where id=1;
1 row updated.
SQL> update hruser.test_data2 set id =3 where id=1;
1 row updated.
SQL> select sid,id1,id2,type,lmode,request from v$lock where sid=(select sid from v$mystat where rownum <=1);
SID ID1 ID2 TY LMODE REQUEST
---------- ---------- ---------- -- ---------- ----------
1151 73096 0 TM 3 0
1151 134 121909014 AE 4 0
1151 73383 0 TM 3 0
1151 458776 815 TX 6 0
两条 DML 产生两个 TM 3,分别对应两张表,但只有一个 TX 6,因为仍然是同一个事务。AE 是 edition 相关的锁,可以忽略。
v$lock.id1 在 TM 锁里就是 object_id,可以反查对象:
SQL> select object_name from dba_objects where object_id='73096';
OBJECT_NAME
-------------------------------------------------------------------
TEST_DATA
SQL> select object_name from dba_objects where object_id='73383';
OBJECT_NAME
-------------------------------------------------------------------
TEST_DATA2
排查时不要只看 v$lock 的 type 和 lmode,必须关联 dba_objects,否则不知道锁在哪张表上。
四、DDL 锁与隐式提交
Oracle 执行 DDL 时,内部等价于:
begin
commit;
DDL语句
commit;
exception;
when others then rollback;
end;
DDL 前后都会提交。这意味着在 DDL 之前未提交的 DML 会被静默提交,很多人以为事务还在,其实已经落地了。
复现:
SQL> CREATE TABLE hruser.test_data3(
id NUMBER PRIMARY KEY,
name VARCHAR2(50),
remark VARCHAR2(100),
create_dt DATE
);
Table created.
SQL> INSERT INTO hruser.test_data3(id,name,remark,create_dt)
SELECT
ROWNUM,
'USER_'||ROWNUM,
'测试数据'||ROWNUM,
SYSDATE - MOD(ROWNUM,30)
FROM dual
CONNECT BY ROWNUM <= 3;
3 rows created.
SQL> commit;
Commit complete.
SQL> select * from hruser.test_data3;
ID NAME REMARK CREATE_DT
---------- ---------- ---------- -------------------
1 USER_1 测试数据1 2026/08/19 11:47:22
2 USER_2 测试数据2 2026/08/18 11:47:22
3 USER_3 测试数据3 2026/08/17 11:47:22
SQL> delete from hruser.test_data3 where id=1;
1 row deleted.
会话 2 删除全表,因为都涉及 id=1,开始等待。此时会话 1 执行 truncate:
SQL> truncate table hruser.test_data3;
truncate table hruser.test_data3
*
ERROR at line 1:
ORA-00054: resource busy and acquire with NOWAIT specified or timeout expired
会话 1 的 delete 被 DDL 的隐式 commit 提交了,会话 2 的 delete 随即执行成功,表里只剩两条:
ID NAME REMARK CREATE_DT
---------- ---------- -------------------- -------------------
2 USER_2 测试数据2 2026/08/18 11:47:22
3 USER_3 测试数据3 2026/08/17 11:47:22
这段输出有一个需要说明的地方:truncate 报了 ORA-00054,说明它没有执行成功,也就没有发生隐式提交。如果按输出推断「会话 1 的 DML 被 DDL 提交」,会与报错本身矛盾。更可能的解释是会话 1 在 truncate 之前或之后另行提交了事务,会话 2 的 delete 才得以继续。这里保留原输出,但不要把这段当作隐式提交的证据。
隐式提交是真实存在的机制,只是这个实验没有证明它。要验证,应该在一个会话里执行 DML 不提交,另一个会话执行能成功的 DDL,比如给另一张表加列,然后回原会话执行 rollback,看数据是否还在。
DDL 锁也分两类:
- X 类型:
truncate、drop、alter table drop/add/modify等,操作期间给表加 X 的 TM 锁,阻塞所有 DML。 - S 类型:在线操作,给表加 RS 的 TM 锁。RS 与 RX 兼容,所以在线 DDL 不阻塞 DML。
五、latch:会阻塞 select 的锁
TX 和 TM 不会阻塞普通 select,因为 select 不在表上加锁,而且一致性读保证读到一致的数据。latch 不一样,它可以阻塞 select,典型现象是 library cache lock 或 library cache pin 等待。
latch 是内存中的轻量级锁,用来保护共享内存结构。它很快,但也是稀缺资源。管理不善时,大量 CPU 会消耗在 latch 的获取和自旋上。最典型的是硬解析:大量不共享的 SQL 反复解析,既抢 library cache latch,又烧 CPU。所以 OLTP 系统必须使用绑定变量。
业务高峰期尽量少做 DDL,原因也在这里:DDL 需要修改数据字典,会持有 library cache 相关的锁,可能让大量正在解析或执行的语句排队。
六、等待事件怎么读
阻塞在会话层面表现为等待事件,名字本身就说明了锁的类型:
| 等待事件 | 含义 | 常见原因 |
|---|---|---|
enq: TX - row lock contention | 行级锁冲突 | 两个事务改同一行,或唯一键冲突、位图索引并发更新 |
enq: TM - contention | 表级锁冲突 | 有未提交事务时执行 DDL,或 lock table 与 DML 冲突 |
enq: TX - allocate ITL entry | ITL 槽位不足 | 块内并发事务数超过 INITRANS,且 MAXTRANS 受限 |
library cache lock / library cache pin | 共享池对象锁 | 硬解析风暴、DDL 与执行并发 |
cursor: pin S wait on X | 游标互斥 | 高并发执行同一 SQL 时发生重编译 |
enq: TX - row lock contention 的 p1 是 usn<<16 | slot,p2 是 seq,p3 是请求的锁模式(4 表示共享,6 表示排它)。用 p3 可以区分是普通行锁还是唯一键冲突:唯一键冲突通常是两个会话插入相同键值,第二个会话在索引上等待。
ITL 等待容易被忽略。事务槽由 initrans 和 maxtrans 决定,块内槽位够时,多个事务可以同时挂在一个块上;槽位不够时,后来的事务要么等,要么动态分配。频繁 allocate ITL entry 说明块内并发度过高,常见于热块上的单行更新。解决办法是调大 initrans 并 rebuild 表,或者从业务上打散热点。
七、把阻塞查出来
7.1 阻塞树
select a.inst_id,
a.process,
a.sid,
a.serial#,
a.sql_id,
a.event,
a.status,
a.program,
a.machine,
connect_by_isleaf as isleaf,
sys_connect_by_path(a.SID || '@' || a.inst_id,' <- ') tree,
level as tree_level
from gv$session a
start with a.blocking_session is not null
connect by (a.sid || '@' || a.inst_id) = prior
(a.blocking_session || '@' || a.blocking_instance);

这是 RAC 环境也能用的写法,用 inst_id 拼接避免跨实例 sid 重复。start with 从所有被阻塞的会话开始,向上追溯阻塞者,tree 列直接给出链路,tree_level 是层级。
单机环境可以把 gv$session 换成 v$session,去掉 inst_id 相关列。执行结果中,叶子节点是最末端的等待者,根节点才是真正持锁的会话。
7.2 行级等待:看 request 和 block
会话 1:
SQL> select sid from v$mystat where rownum <=1;
SID
----------
1577
SQL> select * from hruser.test_data3 where id=2 for update;
ID NAME REMARK CREATE_DT
---------- ---------- ---------- -------------------
2 USER_2 测试数据2 2026/08/18 11:47:22
会话 2:
SQL> select sid from v$mystat where rownum <=1;
SID
----------
1605
SQL> update hruser.test_data3 set id=4 where id=2;
会话 2 被阻塞。会话 3 查锁:
select sid,type,lmode,request,block from v$lock where sid in (1577,1605);
SID TY LMODE REQUEST BLOCK
---------- -- ---------- ---------- ----------
1605 TX 0 6 0
1577 TX 6 0 1
1605 TM 3 0 0
1577 TM 3 0 0
读法:
1577的 TX 是lmode=6、block=1,它持有排它锁并阻塞了别人。1605的 TX 是lmode=0、request=6,它在请求一个 TX 排它锁。- 两个会话的 TM 都是 3,互相兼容,不构成阻塞。
block=1 表示该锁阻塞了其他会话,request>0 表示正在等待。这两个字段比 status 更可靠。
7.3 锁与对象关联
set line 200
col object_name format a20
col user_name format a20;
select /*+ rule */ lpad('--', decode(b.block,1,0,4)) || s.username user_name,b.type,o.owner || ',' || o.object_name object_name,
s.sid,s.serial#,decode(b.REQUEST,0,'BLOCKED','WAITING') status
from dba_objects o, v$session s, v$lock v, v$lock b
WHERE v.ID1 = o.object_id
AND v.SID = s.sid
and v.SID = b.SID
and (b.BLOCK = 1 Or b.REQUEST > 0)
and V.TYPE = 'TM'
order by b.ID2, v.ID1, User_name desc;
USER_NAME TY OBJECT_NAME SID SERIAL# STATUS
-------------------- -- -------------------- ---------- ---------- -------
SYS TX HRUSER,TEST_DATA3 1577 14607 BLOCKED
--SYS TX HRUSER,TEST_DATA3 1605 54552 WAITING
lpad 用两个空格缩进被阻塞者,形成层级;decode(b.REQUEST,0,'BLOCKED','WAITING') 区分持有者和等待者。注意这条语句限定 V.TYPE='TM',所以它显示的是表级锁的归属,用来回答「哪张表被锁住了」,不是行级冲突的完整列表。行级冲突仍要回到 v$lock 看 TX 的 request 和 block。
7.4 表级等待:TM 阻塞
用 lock table ... in share mode 模拟建索引持有的 S 锁:
会话 1:
SQL> select sid from v$mystat where rownum <=1;
SID
----------
1577
SQL> lock table hruser.test_data3 in share mode;
Table(s) Locked.
会话 2:
SQL> select sid from v$mystat where rownum <=1;
SID
----------
1605
SQL> update hruser.test_data3 set id=2 where id=1;
会话 2 被阻塞。会话 3 用同一条关联查询:
USER_NAME TY OBJECT_NAME SID SERIAL# STATUS
-------------------- -- -------------------- ---------- ---------- -------
SYS TM HRUSER,TEST_DATA3 1577 32282 BLOCKED
--SYS TM HRUSER,TEST_DATA3 1605 33583 WAITING
这次阻塞发生在 TM 层:1577 持有 S(4),1605 请求 RX(3),按兼容矩阵冲突。会话 2 连 TX 都还没加上,所以 v$lock 里看不到它的 TX 条目。
这就是行级等待和表级等待的区别:TX 等待只影响同一行,TM 等待会让整张表的所有 DML 排队。
八、处理顺序
查到阻塞后不要直接 kill。按下面的顺序判断:
- 确认阻塞源。用阻塞树找到根节点,确认它的
sid、serial#、sql_id、status和event。根节点如果是INACTIVE且事务未提交,基本就是应用忘了 commit。 - 确认影响面。同一个根节点下挂了多少会话,涉及哪些模块。一个未提交事务挂住几十个连接,和只挂住一个批处理,处理方式不同。
- 优先让应用提交或回滚。能联系到业务方时,这是唯一不丢数据的做法。
- 需要强制终止时,用
alter system kill session 'sid,serial#'。执行后会话进入KILLED,由 PMON 回滚事务,大事务可能持续很久,期间仍占着 undo 和锁。RAC 环境要加@inst_id,否则可能杀到另一个实例的同号会话。 - 处理完再查一次
v$lock和阻塞树,确认锁已释放,不要只看命令返回成功。
如果根节点是 INACTIVE 且有未提交事务,也可能是客户端异常断开后会话还没被 PMON 清理。先看 v$session.status 和 last_call_et,再决定是等 PMON 回收还是手动 kill。
alter system kill session 不回滚已提交的数据,但会回滚未提交部分。终止前确认该事务能否重做,以及回滚期间对 undo 表空间和 I/O 的压力。
九、减少阻塞的几条硬规则
- 事务尽量短,做完就提交。不要把用户交互、文件上传、外部接口调用放在事务中间。
- 批量操作分批提交,避免一个事务持有大量行锁几十分钟。
- 避免跨库分布式事务。链路越长,失败和悬挂的概率越高,锁的持有时间也越不可控。
- OLTP 必须用绑定变量,减少硬解析和 library cache latch 竞争。
- 业务高峰不做 DDL。确需在线变更时,确认操作是 S 类型还是 X 类型,X 类型一定阻塞 DML。
- 应用层加超时和重试,但要区分「锁等待超时」和「业务失败」,否则重试会放大阻塞。
十、事后定位:锁已经释放了怎么办
现场处理完,锁往往已经消失,v$lock 查不到任何东西。这时候要靠历史数据还原。
如果开了 AWR,查 dba_hist_active_sess_history,按等待事件过滤:
SELECT sample_time, inst_id, sid, serial#, sql_id, event, p1, p2, p3, blocking_session, blocking_session_serial#
FROM dba_hist_active_sess_history
WHERE event LIKE 'enq: TX%'
AND sample_time > SYSDATE - 1
ORDER BY sample_time;
blocking_session 有值时,直接就是阻塞源。没有值时,用 p1、p2、p3 反推:TX 的 p1 是 undo 段号和槽号,p2 是序列号,p3 是锁模式。用 p1 可以在 v$transaction 里找到对应事务,再关联 v$session。
如果连 AWR 都没有,只能看告警日志。死锁会写 ORA-00060 并生成 trace,trace 里有两个会话各自的 SQL、持有和等待的锁、以及被回滚的语句。死锁是 Oracle 自动检测并牺牲一个会话,不需要人工 kill,但必须找到根因,否则会反复发生。
死锁的典型模式是两个事务以相反顺序更新两行:A 更新行 1 再更新行 2,B 更新行 2 再更新行 1。解决办法是统一加锁顺序,比如都按主键升序更新,或者在应用层串行化。
十一、要点
- 先 TM 后 TX。DML 加 RX 的 TM 和排它的 TX,两者都成功才算拿到行锁。
- RX 与 RX 兼容,所以多个会话能同时 DML 同一张表;冲突只发生在同一行的 TX 上。
v$lock里block=1是持有并阻塞,request>0是等待;只出现一个 TX 条目很正常,它代表事务。- TM 锁必须关联
dba_objects才能知道锁的是哪张表,id1就是object_id。 - DDL 会隐式提交之前的事务,这是很多「数据莫名其妙变了」的根源。
- latch 可以阻塞
select,TX/TM 不会;高峰期少做 DDL、多用绑定变量。 - kill 会话是最后手段,大事务回滚可能比等待更久。
待核对
- 原文没有给出 TX/TM 兼容矩阵,本文第 2.3 节的矩阵按 Oracle 标准锁兼容关系整理,未逐版本验证。
select ... for update的表级锁模式:Oracle 文档描述为 RS,本文实验输出为 TM 3(RX),两者不一致,需在目标版本上实测确认。- 原文的会话号(1151、1577、1605)、
object_id(73096、73383)和测试表结构均来自原文环境,复现时需替换。 - 原文 6.1 的输出把
1577标为BLOCKED、1605标为WAITING,与常见叫法相反(通常称持锁者为 blocker)。本文保留原输出,但按block和request解释,阅读时以字段为准。 alter system kill session的处理建议为通用做法,原文未涉及。- 第四节 DDL 实验的输出与 ORA-00054 报错矛盾:
truncate失败不应产生隐式提交。原文的结论「会话 1 的 DML 被 DDL 提交」缺乏依据,本文已改为按报错描述,建议在原环境重做该实验确认。
原文首发于微信公众号「止水研习社」。
文章评论