注册
DM8 锁等待、死锁复现与排查分析
培训园地/ 文章详情 /

DM8 锁等待、死锁复现与排查分析

DM_408618 2026/10/16 163 0 0

DM8 锁等待、死锁复现与排查分析

目录


一、实验原理

1.1 什么是行锁

对一行数据执行 UPDATE / DELETE 时,DM8 会为这行加行排他锁(X 锁):

  • X 锁是排他的:同一行数据,同一时间只允许一个事务持有;
  • 其他事务想改这一行,必须等当前持有者 COMMIT / ROLLBACK 释放锁。

1.2 什么是锁等待(阻塞)

  • 单向等待:事务 A 持有 ID=1 的行锁;事务 B 想修改 ID=1,只能阻塞等待。
  • 阻塞不会自动解除,除非 A 提交/回滚,或 DBA 杀掉 A 的会话。

1.3 什么是死锁

  • 循环等待:事务 A 持有 ID=1、等待 ID=2;事务 B 持有 ID=2、等待 ID=1。
  • 两者互相等对方,谁都推进不了,形成僵局。
  • DM8 有死锁检测机制:检测到循环等待后,主动回滚其中一个事务并抛出报错 -6403,另一个事务继续执行。这是数据库正常的自我保护。

本实验利用的正是:两个事务更新多行的顺序不一致(A 先改 1 再改 2;B 先改 2 再改 1),制造交叉等待。


二、实验环境准备

2.1 打开两个终端窗口

  • 窗口 1 → 会话 A
  • 窗口 2 → 会话 B

两个窗口都登录到同一实例:

./disql SYSDBA/Dameng123@localhost:6003

2.2 建测试表并造数据(会话 A 执行)

CREATE TABLE T_LOCK_TEST( ID INT PRIMARY KEY, NAME VARCHAR(20) ); INSERT INTO T_LOCK_TEST VALUES(1,'原始A'); INSERT INTO T_LOCK_TEST VALUES(2,'原始B'); INSERT INTO T_LOCK_TEST VALUES(3,'王五'); COMMIT; SELECT * FROM T_LOCK_TEST;

image.png

预期输出:

ID   NAME
1    原始A
2    原始B
3    王五

image.png

⚠️ 关键前置:这三条 INSERT 之后必须 COMMIT 提交,否则两个会话都读不到这些行,后面 UPDATE 会返回"影响行数 0",锁实验无法进行(本实验踩过的坑,详见第九节)。

为什么用主键:ID 是主键,UPDATE 走主键索引、加行级 X 锁,而不是锁全表,保证能精确制造行级死锁。

2.3 关于"开启事务"的写法(重点,避开多行坑)

disql 里 BEGIN; 写完回车容易进入多行缓冲区,新手容易误判为"卡住/不支持"。本实验统一用更直观的写法:

SET TRANSACTION READ WRITE; -- 等价于 BEGIN;,执行完直接回到 SQL> 提示符

SET TRANSACTION READ WRITE; 与 BEGIN; 功能完全等价,都用于开启一个可读写事务。


三、实验一:制造死锁(核心)

⚠️ 关键:以下步骤在两个窗口按顺序执行;所有 UPDATE 执行成功后都不要 COMMIT / ROLLBACK,否则锁一释放,死锁就无法复现。

步骤 1:会话 A 开启事务并锁定 ID=1

在会话 A执行(分两条,逐条回车):

SET TRANSACTION READ WRITE; UPDATE T_LOCK_TEST SET NAME = 'A修改' WHERE ID = 1;

预期输出: 影响行数 1(成功,ID=1 的行锁已被会话 A 持有)——先不要 COMMIT
企业微信截图_17918716297974.png

步骤 2:会话 B 开启事务并锁定 ID=2

切到会话 B执行:

SET TRANSACTION READ WRITE; UPDATE T_LOCK_TEST SET NAME = 'B修改' WHERE ID = 2;

预期输出: 影响行数 1(成功,ID=2 的行锁已被会话 B 持有)——先不要 COMMIT
image.png

此时:A 持有 ID=1 的锁,B 持有 ID=2 的锁,互不冲突,各自正常。

步骤 3:会话 A 尝试修改 ID=2(会卡住)

回到会话 A执行:

UPDATE T_LOCK_TEST SET NAME = 'A改2' WHERE ID = 2;

企业微信截图_17918719211370.png

预期现象: 这条语句卡住不动(没有返回影响行数),因为 ID=2 的行锁在会话 B 手里,A 在等 B。

步骤 4:会话 B 尝试修改 ID=1(触发死锁!)

切到会话 B执行:

UPDATE T_LOCK_TEST SET NAME = 'B改1' WHERE ID = 1;

企业微信截图_17918723535269.png
预期现象: 几乎瞬间抛出死锁报错:

[-6403]死锁

步骤 5:观察"幸存者"

  • 触发死锁后,会话 B 的事务被 DM8 自动回滚(它抛出 死锁);
  • 会话 A 在步骤 3 卡住的 UPDATE ... WHERE ID=2 会解除阻塞,立即执行成功。

在会话 A确认:

SELECT * FROM T_LOCK_TEST;

预期输出: ID=2 的 NAME 已变为 A改2(A 的事务执行成功)。

💡 DM8 自动选择回滚"牺牲"一个事务来打破死锁。具体牺牲谁不固定(取决于检测时机)

步骤 6:提交幸存事务

在会话 A执行:

COMMIT;

四、实验二:锁等待(无死锁,仅阻塞)

单独做一组,体会"单向阻塞"与死锁的区别。用 ID=3,避免与上面冲突。

步骤 1:会话 A 锁定 ID=3

SET TRANSACTION READ WRITE; UPDATE T_LOCK_TEST SET NAME = 'A锁3' WHERE ID = 3;

image.png
(确认输出 影响行数 1,不要提交)

步骤 2:会话 B 修改同一行 ID=3

UPDATE T_LOCK_TEST SET NAME = 'B改3' WHERE ID = 3;

预期现象: 卡住不动,进入锁等待(阻塞),但不会报死锁——单向等待没有循环。

步骤 3:观察阻塞(开第三个窗口执行)

SELECT TRX_ID, LTYPE, LMODE, BLOCKED FROM V$LOCK WHERE BLOCKED = 1;

image.png

预期输出: 能查到会话 B 被阻塞,BLOCKED = 1。

步骤 4:解除阻塞

方式一(业务方提交,释放锁):

-- 在会话 A 执行 COMMIT;

会话A执行COMMIT;
image.png

会话B表现,可以看到当前语句执行时间为2分53秒,证明会话B的SQL语句被阻塞了。
image.png

方式二(DBA 强制杀会话):

-- 在管理窗口执行,把上面查到的 SESSION_ID 填进去 SP_CLOSE_SESSION(会话B的SESSION_ID);

五、死锁排查操作(重点)

5.1 查询死锁历史:V$DEADLOCK_HISTORY

⚠️ 注意:死锁发生时间的字段名是 HAPPEN_TIME,会话 ID 是 SESS_ID(不是 DEADLOCK_TIME / SESSION_ID)。

SELECT HAPPEN_TIME, SESS_ID, SQL_TEXT, TRX_ID FROM V$DEADLOCK_HISTORY; -- 查看全部字段 SELECT * FROM V$DEADLOCK_HISTORY;

用法: 看哪条 SQL、哪个会话触发死锁,以及发生时间。

5.2 查看当前锁信息:V$LOCK

⚠️ 注意:V$LOCK 本身没有 SESS_ID 字段。原生字段为:ADDR, TRX_ID, LTYPE, LMODE, BLOCKED, TABLE_ID, ROW_IDX, TID。

SELECT TRX_ID, LTYPE, LMODE, BLOCKED, TABLE_ID, ROW_IDX FROM V$LOCK;

字段说明:

  • LTYPE:锁类型(TID=行锁,OBJECT=对象锁)
  • LMODE:锁模式(X=排他锁,S=共享锁)
  • BLOCKED=1:代表该锁正在发生等待(有会话被阻塞)

5.3 完整关联查询(推荐:同时看到会话ID、事务、锁、SQL)

把锁信息与会话信息关联起来:

SELECT S.SESS_ID, L.TRX_ID, L.LTYPE, L.LMODE, L.BLOCKED, L.TABLE_ID, O.NAME AS TABLE_NAME, S.SQL_TEXT, S.CLNT_IP FROM V$LOCK L LEFT JOIN V$SESSIONS S ON L.TRX_ID = S.TRX_ID LEFT JOIN SYSOBJECTS O ON L.TABLE_ID = O.ID;

用法: 一次性看到哪个会话、持有什么锁、操作哪张表、正在执行什么 SQL。

5.4 专门看阻塞等待关系(阻塞发生时使用)

SELECT DS.SESS_ID AS 等待的会话ID, DS.TRX_ID AS 等待事务ID, SS.SESS_ID AS 持有锁会话ID, SS.TRX_ID AS 持有锁事务ID, L.LTYPE, L.LMODE FROM V$LOCK L LEFT JOIN V$SESSIONS DS ON DS.TRX_ID = L.TRX_ID LEFT JOIN V$SESSIONS SS ON SS.TRX_ID = L.TID WHERE L.BLOCKED = 1;

5.5 查看阻塞等待事务链:V$TRXWAIT(可选)

SELECT ID, WAIT_FOR_ID, WAIT_TIME, THRD_ID FROM V$TRXWAIT;
  • ID:当前等待的事务ID
  • WAIT_FOR_ID:被等待、持有锁的事务ID

5.6 查看被阻塞会话正在执行的 SQL:V$SESSIONS

SELECT SESS_ID, SQL_TEXT, STATE, TRX_ID FROM V$SESSIONS WHERE SESS_ID = <上一步查到的会话ID>;

5.7 查看事务信息:V$TRX

SELECT ID, START_TIME, ISOLATION, STATUS FROM V$TRX;

六、死锁与锁等待的核心区别

对比项 锁等待(阻塞) 死锁
现象 会话一直 hang 住,等待资源,不会自动报错 数据库检测到循环等待,主动回滚一个事务,抛出 -6403
资源关系 单向等待:A 持有锁,B 等 A 循环等待:A 等 B,B 同时等 A
自动处理 不会自动处理,需人工提交/回滚/杀会话 数据库死锁检测器自动处理
日志体现 无特殊报错,仅阻塞 抛 -6403,记录在 V$DEADLOCK_HISTORY

七、常见根因与最佳实践

根因分析

  1. 业务更新多行数据的顺序不一致(本次实验的根本原因):A 先改 1 再改 2,B 先改 2 再改 1,交叉等待 → 死锁。
  2. 长事务:开启事务后长时间不提交,锁长期持有,增大锁等待和死锁
  3. 一次性更新大量行:容易触发锁升级,扩大锁范围。

最佳实践

  1. 统一更新顺序:多行更新按固定顺序(如按 ID 从小到大),避免交叉锁。
  2. 缩短事务:尽早 COMMIT,减少锁持有时间。
  3. 控制事务规模:大批量操作拆分小批量,逐批提交。
  4. 合理索引:确保 UPDATE/DELETE 走索引,避免行锁升级为表锁。

八、清理实验环境

COMMIT; -- 各会话先提交/回滚,释放残余锁 DROP TABLE T_LOCK_TEST;

九、实操踩坑记录

以下坑是实际跑实验时踩到的,整理出来方便你避坑,也可作为博文素材。

坑 1:基础数据没有提前 COMMIT,导致 UPDATE"影响行数 0"

  • 现象:INSERT 后没提交,UPDATE 返回 影响行数 0,死锁无法复现。
  • 原因:没有 COMMIT,两个会话读不到那几行;影响行数 0 表示 WHERE 没匹配到行,不加任何行锁。
  • 解决:造数据后必须 COMMIT;,并先 SELECT * 确认数据存在。

坑 2:BEGIN; 写完回车进入多行缓冲区,被误判为"不支持 BEGIN"

  • 现象:BEGIN; 回车后出现行号 2,或 UPDATE 停在多行编辑。
  • 原因:disql 交互下,分号只是语句结束标记,回车只换行、不立即执行(需在行号处输入 / 才执行缓冲区)。
  • 解决:本实验统一用 SET TRANSACTION READ WRITE; 开启事务,执行完直接回到 SQL>,避开多行混淆。

坑 3:BEGIN; UPDATE ...; 写同一行再用 / 执行,报语法错误 -2007

  • 现象:把两条语句放一行用 / 执行,报 [-2007] 语法分析出错。
  • 原因:disql 对"同一条缓冲脚本里拼接多条语句再整体执行"的解析不友好。
  • 解决:一条 SQL 一行,逐条回车执行,不用 /。

坑 4:实验过程中误执行 COMMIT,锁提前释放,实验失败

  • 现象:UPDATE 后立刻执行了 COMMIT,两个会话的锁都不存在,后续没有阻塞也没有死锁。
  • 原因:COMMIT 释放所有行锁。
  • 解决:两个会话全程禁止 COMMIT / ROLLBACK,直到确认触发死锁。

坑 5:实验前后数据被改动,重跑时需要重置数据

  • 现象:上一轮 COMMIT 后 ID1/ID2 已被改成实验值,重跑实验条件不对。
  • 解决:重跑前先 UPDATE ... SET NAME='原始A'/'原始B' 并 COMMIT,重置为初始值。

坑 6:V$LOCK 视图字段名用错,报 -2111 无效列名

  • 现象:SELECT SESS_ID,TRX_ID,OBJ_ID,TYPE,MODE,ROW_ID FROM V$LOCK; 报 [-2111] 无效的列名[SESS_ID]。
  • 原因:V$LOCK 没有 SESS_ID / OBJ_ID / TYPE / MODE 字段。会话 ID(SESS_ID)在 V$SESSIONS 中;V$LOCK 原生字段是 ADDR,TRX_ID,LTYPE,LMODE,BLOCKED,TABLE_ID,ROW_IDX,TID。
  • 解决:锁信息单独看用 SELECT TRX_ID,LTYPE,LMODE,BLOCKED,TABLE_ID,ROW_IDX FROM V$LOCK;;要看会话ID,用 V$LOCK LEFT JOIN V$SESSIONS ON L.TRX_ID=S.TRX_ID 关联查询。

坑 7:V$DEADLOCK_HISTORY 字段名用错,报 -2111 无效列名

  • 现象:SELECT DEADLOCK_TIME, SESSION_ID, SQL_TEXT, TRX_ID FROM V$DEADLOCK_HISTORY; 报 [-2111] 无效的列名[DEADLOCK_TIME]。
  • 原因:死锁发生时间字段是 HAPPEN_TIME,会话 ID 是 SESS_ID,不是 DEADLOCK_TIME / SESSION_ID。
  • 解决:用 SELECT HAPPEN_TIME, SESS_ID, SQL_TEXT, TRX_ID FROM V$DEADLOCK_HISTORY;。

十、常见问题 FAQ

Q1:执行 UPDATE 后一直不返回,是不是死锁了?
不一定是。多数是锁等待(单向阻塞),只有抛 -6403 才是死锁。可查 V$LOCK WHERE BLOCKED=1 确认。

Q2:为什么死锁牺牲的有时是 A、有时是 B?
死锁检测时机不同,DM8会选择检测到死锁时的一个事务回滚,具体不固定。关键是"出现死锁、一个事务被回滚、另一个能继续",说明检测机制生效。

Q3:怎么查死锁的具体 SQL?
查 V$DEADLOCK_HISTORY 的 SQL_TEXT 字段。

Q4:生产环境出现死锁怎么办?
死锁已被数据库自动处理,不会长期阻塞;重点是从根因减少死锁(统一更新顺序、缩短事务)。频繁死锁则结合 V$DEADLOCK_HISTORY 分析是哪类业务 SQL 造成。

Q5:锁等待太久怎么办?
定位持锁会话(V$LOCK、V$SESSIONS),通知业务提交;紧急时用 SP_CLOSE_SESSION(会话ID) 强制断开。


评论
后发表回复

作者

文章

阅读量

获赞

扫一扫
联系客服