Oracle TM 锁 Mode 4(Share)问题排查总结
一、概述
Oracle的**TM锁(表级锁,Table Lock)**是保障DML操作与DDL操作隔离的核心机制,是一种表级锁,保护表结构,在DML操作期间获取。
TM锁模式
| 模式 | 名称 | 缩写 | 典型操作 |
|---|---|---|---|
| 1 | Null | N | 某些内部操作 |
| 2 | Row Share | RS/SS | SELECT ... FOR UPDATE |
| 3 | Row Exclusive | RX/SX | INSERT / UPDATE / DELETE |
| 4 | Share | S | CREATE INDEX ONLINE、外键未建索引时对父表 DML |
| 5 | Share Row Exclusive | SRX/SSX | 某些 DDL |
| 6 | Exclusive | X | LOCK TABLE ... IN EXCLUSIVE MODE、DDL |
二、TM Mode 4模式的操作总结
显式执行 LOCK TABLE 命令(最常见)
DBA或应用主动执行了表级锁定语句:
LOCK TABLE your_table IN SHARE MODE;
这通常用于保证在批量处理期间表结构不变,或为了配合外键做级联操作前的准备。
传统导出工具(exp)的 CONSISTENT=Y
早年的Oracle exp 工具,如果加上 CONSISTENT=Y 参数,为保证导出数据在时间点上的全局一致性,会对导出的表施加 Mode=4 S锁,防止导出过程中数据发生变更。(备注:新版 expdp(数据泵)默认使用Mode=5,行为有所不同)
物化视图(Materialized View)完全刷新
当执行 REFRESH COMPLETE 且采用**原子刷新(ATOMIC_REFRESH=>TRUE)**时,在刷新阶段,Oracle可能会对基表持有Mode=4 S锁,以防止刷新期间基表数据被修改,确保快照一致性。
外键未索引导致的"隐形"S锁(生产中极高的踩坑点)
这是大多数生产环境意外出现Mode=4锁的根本原因 。
如果子表(外键表)的外键列上没有索引 ,当父表的主键/唯一键发生 UPDATE 或 DELETE 时,Oracle为了维护参照完整性(检查子表是否有对应记录),会在子表上自动施加一个 Mode=4 S锁(而不是行级锁),导致整个子表的DML被阻塞。
举例 :
ORDERS表是子表(有CUSTOMER_ID外键)。如果CUSTOMERS表更新了ID=1的主键值,而ORDERS.CUSTOMER_ID没有索引,ORDERS整张表会被加S锁,此时任何其他会话都无法向ORDERS插入新订单。
分区维护操作(如 EXCHANGE PARTITION)
在执行 ALTER TABLE ... EXCHANGE PARTITION 交换分区时,Oracle会对涉及的表短暂持有S锁,完成元数据切换。
三、外键未索引导致的TM 锁 Mode 4案例
当子表的外键列没有建索引 时,对父表的 UPDATE 或 DELETE 操作需要在子表上获取 mode 4(Share 锁),目的是防止在检查外键约束期间子表被并发修改。
1、环境准备
-- 1. 创建父表和子表
CREATE TABLE parent (
id NUMBER PRIMARY KEY,
name VARCHAR2(50)
);
CREATE TABLE child (
id NUMBER PRIMARY KEY,
parent_id NUMBER,
data VARCHAR2(50),
CONSTRAINT fk_child_parent FOREIGN KEY (parent_id) REFERENCES parent(id)
-- 注意:parent_id 列上没有建索引!
);
-- 2. 插入测试数据
INSERT INTO parent VALUES (1, 'PARENT_1');
INSERT INTO parent VALUES (2, 'PARENT_2');
INSERT INTO child VALUES (101, 1, 'CHILD_1');
INSERT INTO child VALUES (102, 1, 'CHILD_2');
INSERT INTO child VALUES (103, 2, 'CHILD_3');
COMMIT;
-- 会话 A
UPDATE child SET data = 'MODIFIED' WHERE id = 101;
-- 未提交,持有 child 表的 TM 锁 mode=3(Row Exclusive)
-- 会话 B
DELETE FROM parent WHERE id = 1;
-- 需要检查子表的外键约束
-- 需要在 child 表上获取 TM 锁 mode=4(Share)
-- 但会话 A 持有 mode=3,与 mode=4 互斥
-- → 会话 B 被阻塞,等待 mode 4
2、查询分析
1)通过gv$lock视图查询,检查数据中是否存在锁等待
SESS LMODE CTIME INST_ID ID1 ID2 REQUEST TY
---------- --------------- ---------- ---------- ---------- ---------- ---------- --
Holder: 64 row exclusive 42 1 87488 0 0 TM
Waiter: 62 none 22 1 87488 0 4 TM
--可以看到TYPE为TM表锁,request=4,即mode=4请求模式!
2)、通过ID1,查询具体的表对象(表锁TM模式下,ID1表示对象id)
col owner format a10
col object_name format a15
col object_type format a15
select owner,object_name,object_type from dba_objects a where object_id=87488;
OWNER OBJECT_NAME OBJECT_TYPE
---------- --------------- ---------------
USER1 CHILD TABLE
3)查询具体的TX锁等待会话,等待会话正在执行的语句,正在等待的对象,会话自身的连接属性等详细信息(建议使用连接工具查询,而非sqlplus方式)
SQL_ID EVENT USERNAME OWNER OBJECT_NAME OBJECT_TYPE SID BLOCKING_INSTANCE BLOCKING_SESSION
3n86yupq0t0ca enq: TM - contention USER1 USER1 CHILD TABLE 62 1 64
--可以堵塞sid为64,
3、解决方案
在外键列上创建索引,创建后,父表DML对子表仅加行级锁(Mode=3),不再锁整表(mode=4)
四、查询数据库中哪些外键未创建索引
set linesize 200 pagesize 999
col owner format a20
col table_Name format a20
col FK_COLUMNS format a20
WITH fk_cols AS (
-- 步骤1:聚合所有外键的列(按顺序拼接)
SELECT
c.owner,
c.table_name,
c.constraint_name AS fk_name,
LISTAGG(cc.column_name, ',' ) WITHIN GROUP (ORDER BY cc.position) AS fk_columns
FROM
dba_constraints c
JOIN dba_cons_columns cc ON c.owner = cc.owner AND c.constraint_name = cc.constraint_name
WHERE
c.constraint_type = 'R' -- R 代表外键
-- 排除 Oracle 系统内置用户
AND c.owner NOT IN (
'SYS','SYSTEM','DBSNMP','SYSMAN','OUTLN','MDSYS',
'ORDSYS','EXFSYS','DMSYS','WMSYS','CTXSYS','ANONYMOUS',
'XDB','ORDPLUGINS','OLAPSYS','APPQOSSYS','APEX_030200',
'APEX_040000','APEX_040200','APEX_050000','APEX_PUBLIC_USER',
'FLOWS_FILES','OWBSYS','OWBSYS_AUDIT','ORDDATA','LBACSYS',
'OJVMSYS','GSMADMIN_INTERNAL','MDDATA','SPATIAL_WFS_ADMIN_USR',
'SPATIAL_CSW_ADMIN_USR','DIP','XS$NULL','SI_INFORMTN_SCHEMA'
)
GROUP BY
c.owner, c.table_name, c.constraint_name
),
idx_cols AS (
-- 步骤2:聚合所有索引的列(按顺序拼接)
SELECT
ic.table_owner,
ic.table_name,
ic.index_name,
LISTAGG(ic.column_name, ',' ) WITHIN GROUP (ORDER BY ic.column_position) AS idx_columns
FROM
dba_ind_columns ic
GROUP BY
ic.table_owner, ic.table_name, ic.index_name
)
-- 步骤3:筛选出外键列不是任何索引的"前导列组合"的情况
SELECT
f.owner ,
f.table_name ,
f.fk_name ,
f.fk_columns
FROM
fk_cols f
WHERE
NOT EXISTS (
SELECT 1
FROM idx_cols i
WHERE
i.table_owner = f.owner
AND i.table_name = f.table_name
--匹配逻辑:索引列必须以外键列开头(完全匹配 或 外键列 + 其他列)
AND ( i.idx_columns = f.fk_columns
OR i.idx_columns LIKE f.fk_columns || ',%' )
)
ORDER BY
f.owner, f.table_name;