Aekor

Aekor
专注于用户阅读体验的响应式博客主题
  1. 首页
  2. Oracle 数据库
  3. 正文

Oracle 锁与阻塞:TX、TM、DDL 与 latch 的定位方法

2026-08-20 24063点热度 0人点赞 0条评论

会话卡住、业务超时、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名称典型来源
2RS,row sharelock table ... in row share mode、部分在线 DDL
3RX,row exclusiveinsert、update、delete、merge
4S,sharelock table ... in share mode、部分在线 DDL
5SRX,share row exclusivelock table ... in share row exclusive mode
6X,exclusivetruncate、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 entryITL 槽位不足块内并发事务数超过 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。按下面的顺序判断:

  1. 确认阻塞源。用阻塞树找到根节点,确认它的 sid、serial#、sql_id、status 和 event。根节点如果是 INACTIVE 且事务未提交,基本就是应用忘了 commit。
  2. 确认影响面。同一个根节点下挂了多少会话,涉及哪些模块。一个未提交事务挂住几十个连接,和只挂住一个批处理,处理方式不同。
  3. 优先让应用提交或回滚。能联系到业务方时,这是唯一不丢数据的做法。
  4. 需要强制终止时,用 alter system kill session 'sid,serial#'。执行后会话进入 KILLED,由 PMON 回滚事务,大事务可能持续很久,期间仍占着 undo 和锁。RAC 环境要加 @inst_id,否则可能杀到另一个实例的同号会话。
  5. 处理完再查一次 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 会话是最后手段,大事务回滚可能比等待更久。

待核对

  1. 原文没有给出 TX/TM 兼容矩阵,本文第 2.3 节的矩阵按 Oracle 标准锁兼容关系整理,未逐版本验证。
  2. select ... for update 的表级锁模式:Oracle 文档描述为 RS,本文实验输出为 TM 3(RX),两者不一致,需在目标版本上实测确认。
  3. 原文的会话号(1151、1577、1605)、object_id(73096、73383)和测试表结构均来自原文环境,复现时需替换。
  4. 原文 6.1 的输出把 1577 标为 BLOCKED、1605 标为 WAITING,与常见叫法相反(通常称持锁者为 blocker)。本文保留原输出,但按 block 和 request 解释,阅读时以字段为准。
  5. alter system kill session 的处理建议为通用做法,原文未涉及。
  6. 第四节 DDL 实验的输出与 ORA-00054 报错矛盾:truncate 失败不应产生隐式提交。原文的结论「会话 1 的 DML 被 DDL 提交」缺乏依据,本文已改为按报错描述,建议在原环境重做该实验确认。

原文首发于微信公众号「止水研习社」。

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: Oracle 锁 阻塞排查
最后更新:2026-10-17

Aekor

这个人很懒,什么都没留下

点赞
< 上一篇
下一篇 >

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

使用AI教程

  • API报错解决方案
  • API 基础知识
  • API Key 获取
  • 最新接入教程

分类

  • Blog
  • Oracle 数据库
  • TradingAgents-CN
  • 使用教程
  • 数据库

COPYRIGHT © 2026 Aekor. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang