对一行数据执行 UPDATE / DELETE 时,DM8 会为这行加行排他锁(X 锁):
COMMIT / ROLLBACK 释放锁。ID=1 的行锁;事务 B 想修改 ID=1,只能阻塞等待。ID=1、等待 ID=2;事务 B 持有 ID=2、等待 ID=1。-6403,另一个事务继续执行。这是数据库正常的自我保护。本实验利用的正是:两个事务更新多行的顺序不一致(A 先改 1 再改 2;B 先改 2 再改 1),制造交叉等待。
两个窗口都登录到同一实例:
./disql SYSDBA/Dameng123@localhost:6003
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;
预期输出:
ID NAME
1 原始A
2 原始B
3 王五
⚠️ 关键前置:这三条 INSERT 之后必须 COMMIT 提交,否则两个会话都读不到这些行,后面 UPDATE 会返回"影响行数 0",锁实验无法进行(本实验踩过的坑,详见第九节)。
为什么用主键:
ID是主键,UPDATE 走主键索引、加行级 X 锁,而不是锁全表,保证能精确制造行级死锁。
disql 里 BEGIN; 写完回车容易进入多行缓冲区,新手容易误判为"卡住/不支持"。本实验统一用更直观的写法:
SET TRANSACTION READ WRITE; -- 等价于 BEGIN;,执行完直接回到 SQL> 提示符
SET TRANSACTION READ WRITE;与BEGIN;功能完全等价,都用于开启一个可读写事务。
⚠️ 关键:以下步骤在两个窗口按顺序执行;所有 UPDATE 执行成功后都不要 COMMIT / ROLLBACK,否则锁一释放,死锁就无法复现。
在会话 A执行(分两条,逐条回车):
SET TRANSACTION READ WRITE;
UPDATE T_LOCK_TEST SET NAME = 'A修改' WHERE ID = 1;
预期输出: 影响行数 1(成功,ID=1 的行锁已被会话 A 持有)——先不要 COMMIT
切到会话 B执行:
SET TRANSACTION READ WRITE;
UPDATE T_LOCK_TEST SET NAME = 'B修改' WHERE ID = 2;
预期输出: 影响行数 1(成功,ID=2 的行锁已被会话 B 持有)——先不要 COMMIT
此时:A 持有 ID=1 的锁,B 持有 ID=2 的锁,互不冲突,各自正常。
回到会话 A执行:
UPDATE T_LOCK_TEST SET NAME = 'A改2' WHERE ID = 2;
预期现象: 这条语句卡住不动(没有返回影响行数),因为 ID=2 的行锁在会话 B 手里,A 在等 B。
切到会话 B执行:
UPDATE T_LOCK_TEST SET NAME = 'B改1' WHERE ID = 1;
预期现象: 几乎瞬间抛出死锁报错:
[-6403]死锁
UPDATE ... WHERE ID=2 会解除阻塞,立即执行成功。在会话 A确认:
SELECT * FROM T_LOCK_TEST;
预期输出: ID=2 的 NAME 已变为 A改2(A 的事务执行成功)。
💡 DM8 自动选择回滚"牺牲"一个事务来打破死锁。具体牺牲谁不固定(取决于检测时机)
在会话 A执行:
COMMIT;
单独做一组,体会"单向阻塞"与死锁的区别。用 ID=3,避免与上面冲突。
SET TRANSACTION READ WRITE;
UPDATE T_LOCK_TEST SET NAME = 'A锁3' WHERE ID = 3;
(确认输出 影响行数 1,不要提交)
UPDATE T_LOCK_TEST SET NAME = 'B改3' WHERE ID = 3;
预期现象: 卡住不动,进入锁等待(阻塞),但不会报死锁——单向等待没有循环。
SELECT TRX_ID, LTYPE, LMODE, BLOCKED FROM V$LOCK WHERE BLOCKED = 1;
预期输出: 能查到会话 B 被阻塞,BLOCKED = 1。
方式一(业务方提交,释放锁):
-- 在会话 A 执行
COMMIT;
会话A执行COMMIT;
会话B表现,可以看到当前语句执行时间为2分53秒,证明会话B的SQL语句被阻塞了。
方式二(DBA 强制杀会话):
-- 在管理窗口执行,把上面查到的 SESSION_ID 填进去
SP_CLOSE_SESSION(会话B的SESSION_ID);
⚠️ 注意:死锁发生时间的字段名是
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、哪个会话触发死锁,以及发生时间。
⚠️ 注意: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:代表该锁正在发生等待(有会话被阻塞)把锁信息与会话信息关联起来:
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。
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;
SELECT ID, WAIT_FOR_ID, WAIT_TIME, THRD_ID FROM V$TRXWAIT;
ID:当前等待的事务IDWAIT_FOR_ID:被等待、持有锁的事务IDSELECT SESS_ID, SQL_TEXT, STATE, TRX_ID
FROM V$SESSIONS
WHERE SESS_ID = <上一步查到的会话ID>;
SELECT ID, START_TIME, ISOLATION, STATUS FROM V$TRX;
| 对比项 | 锁等待(阻塞) | 死锁 |
|---|---|---|
| 现象 | 会话一直 hang 住,等待资源,不会自动报错 | 数据库检测到循环等待,主动回滚一个事务,抛出 -6403 |
| 资源关系 | 单向等待:A 持有锁,B 等 A | 循环等待:A 等 B,B 同时等 A |
| 自动处理 | 不会自动处理,需人工提交/回滚/杀会话 | 数据库死锁检测器自动处理 |
| 日志体现 | 无特殊报错,仅阻塞 | 抛 -6403,记录在 V$DEADLOCK_HISTORY |
COMMIT; -- 各会话先提交/回滚,释放残余锁
DROP TABLE T_LOCK_TEST;
以下坑是实际跑实验时踩到的,整理出来方便你避坑,也可作为博文素材。
坑 1:基础数据没有提前 COMMIT,导致 UPDATE"影响行数 0"
影响行数 0,死锁无法复现。影响行数 0 表示 WHERE 没匹配到行,不加任何行锁。COMMIT;,并先 SELECT * 确认数据存在。坑 2:BEGIN; 写完回车进入多行缓冲区,被误判为"不支持 BEGIN"
BEGIN; 回车后出现行号 2,或 UPDATE 停在多行编辑。/ 才执行缓冲区)。SET TRANSACTION READ WRITE; 开启事务,执行完直接回到 SQL>,避开多行混淆。坑 3:BEGIN; UPDATE ...; 写同一行再用 / 执行,报语法错误 -2007
/ 执行,报 [-2007] 语法分析出错。/。坑 4:实验过程中误执行 COMMIT,锁提前释放,实验失败
坑 5:实验前后数据被改动,重跑时需要重置数据
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]。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;。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) 强制断开。
文章
阅读量
获赞
