《DM8系统管理员手册》
《DM8数据共享集群》
1 锁等待现象与原理
锁等待(或称阻塞)和死锁是会与并发事务一起发生的两个事件,它们都与锁相关。
当一个事务正在占用某个资源的锁,此时另一个事务正在请求这个资源上与第一个锁相冲突的锁类型时,就会导致锁等待。
发生锁等待的事务将一直挂起,直到持有锁的事务放弃锁定的资源为止。
死锁与锁等待的不同之处在于死锁包括两个或两个以上发生锁等待的事务,它们之间形成了等待环,每个都等待其他事务释放锁。
例如事务 1 给表 T1 上了排他锁,第二个事务给表 T2 上了排他锁,此时事务 1 请求 T2 的排他锁,就会处于等待状态,导致了锁等待。
若此时 T2 再请求表 T1 的排他锁,则 T2 也处于锁等待状态。
此时这两个事务发生死锁,DM 数据库会选择牺牲掉其中一个事务。
在 DM 数据库中,INSERT、UPDATE、DELETE 是最常见的会产生锁等待和死锁的语句:
- INSERT 语句导致锁等待的++唯一情况++是,当多个事务同时试图向有主键或 UNIQUE 约束的表中插入相同的数据时,其中的一个事务将陷入锁等待,直到另外一个事务提交或回滚。一个事务提交时,另一个事务将收到唯一性冲突的错误;一个事务回滚时,陷入锁等待的事务可以继续执行。
- 当 UPDATE 和 DELETE 语句所修改的记录,已经被另外的事务修改过,也将导致锁等待, 直到另一个事务提交或回滚。
2 常见锁模式分类
锁模式指定并发用户如何访问锁定资源。
DM 数据库使用四种不同的锁模式:共享锁、 排他锁、意向共享锁和意向排他锁。
2.1 共享锁
共享锁(Share Lock,简称 S 锁)用于读操作,防止其他事务修改正在访问的对象。
这种封锁模式允许多个事务同时并发读取相同的资源,但是不允许任何事务修改这个资源。
!attention\] 多版本控制对共享锁的优化 在多版本控制以前,数据库仅通过锁机制来实现并发控制。 数据库对读操作上共享锁, 写操作上排他锁,这种锁机制虽然解决了并发问题,但影响了并发性。 例如,当对一个事务对表进行查询时,另一个对表更新的事务就必须等待。 DM 数据库的多版本实现完全消除了行锁对系统资源的消耗,查询永远不会被阻塞也不需要上行锁,并通过 TID 锁机制消除了插入、删除、更新操作的行锁。数据库的读操作与写操作不会相互阻塞,并发度大幅度提高。
2.2 排他锁
排他锁(Exclusive Lock,简称 X 锁)用于写操作,以独占的方式访问对象,不允许任何其他事务访问被封锁对象;防止多个事务同时修改相同的数据,避免引发数据错误;防止访问一个正在被修改的对象,避免引发数据不一致。一般在修改对象定义时使用。
2.3 意向锁
意向锁(Intent Lock)用于读取或修改被访问对象数据时使用,多个事务可以同时对相同对象上意向锁,DM 支持两种意向锁:
- 意向共享锁(Intent Share Lock,简称 IS 锁):一般在只读访问对象时使用;
- 意向排他锁(Intent Exclusive Lock,简称 IX 锁):一般在修改对象数据时使用。
四种锁模式的相容矩阵如下表所示,其中"Y"表示相容;"N"表示不相容。
如表中第二行第二列为"Y",表示如果已经加了 IS 锁时,其他用户还可以继续添加 IS 锁,第二行第五列为 "N",表示如果已经加了 IS 锁时,其他用户不能添加 X 锁。

!NOTE\] 达梦封锁机制优化 达梦通过**TID锁** 和**对象锁**减少封锁冲突: * **TID锁**:以事务号为封锁对象,替代传统行锁。INSERT/UPDATE/DELETE操作时,通过物理记录的TID字段隐式加X锁,避免大量行锁的资源消耗,仅在多事务修改同一行时生成新TID锁。 * **对象锁**:合并数据字典锁和表锁,通过对象ID封锁,减少DDL/DML并发冲突,提升系统性能。
3 常见锁等待模拟及查询
测试环境构建如下:
sql
-- 创建测试表
CREATE TABLE t_test_lock
(
"ID" INT NOT NULL,
"NAME" VARCHAR(50),
NOT CLUSTER PRIMARY KEY("ID")) STORAGE(ON "MAIN", CLUSTERBTR) ;
-- 插入测试数据
INSERT INTO t_test_lock VALUES (1, 'Alice'), (2, 'Bob');
COMMIT;
3.1 场景一:S锁与X锁冲突(读阻塞写)
S锁允许多个事务并发读,但阻止其他事务加X锁(修改)。
操作步骤如下:
sql
-- 1 会话 A 开启事务,执行查询(默认加S锁)
SELECT * FROM t_test_lock WHERE ID = 1;
-- 保持事务不提交
-- 2 会话 B(写操作,请求X锁)
UPDATE t_test_lock SET NAME = 'Charlie' WHERE ID = 1;
但实际上由于达梦默认使用读提交(READ COMMITTED)隔离级别,并实现了多版本并发控制(MVCC),它的一个重要特性是:
普通的 SELECT 查询不会被阻塞,也不会阻塞其他事务的写操作(UPDATE/DELETE/INSERT)。
读操作通过回滚段读取数据的历史版本,不会加传统的 S 锁。
会话 A 的 SELECT * FROM t_test_lock WHERE ID = 1; 是快照读,不会在行上加 S 锁。
会话 B 的 UPDATE 可以直接获取 X 锁并修改数据,不会发生阻塞。

3.2 场景二:INSERT唯一约束冲突阻塞
多个事务插入相同主键值时,后插入的事务会被阻塞,直到先插入的事务提交或回滚。
操作步骤如下:
sql
-- 1 会话 A 开启事务,执行插入
INSERT INTO t_test_lock VALUES (3, 'David');
-- 保持事务不提交
-- 2 会话 B(写操作,请求X锁)
INSERT INTO t_test_lock VALUES (3, 'Eve');

查询阻塞会话链路
sql
SELECT
W.ID AS 等待事务ID,
W.WAIT_FOR_ID AS 被等事务ID,
W.WAIT_TIME AS 等待时间_MS,
S_WAIT.SESS_ID AS 等待会话ID,
S_WAIT.USER_NAME AS 等待用户名,
S_WAIT.CLNT_IP AS 等待客户端IP,
S_WAIT.SQL_TEXT AS 等待SQL,
S_BLK.SESS_ID AS 阻塞会话ID,
S_BLK.USER_NAME AS 阻塞用户名,
S_BLK.CLNT_IP AS 阻塞客户端IP,
S_BLK.SQL_TEXT AS 阻塞SQL
FROM V$TRXWAIT W
LEFT JOIN V$TRX T_WAIT ON W.ID = T_WAIT.ID
LEFT JOIN V$SESSIONS S_WAIT ON T_WAIT.SESS_ID = S_WAIT.SESS_ID
LEFT JOIN V$TRX T_BLK ON W.WAIT_FOR_ID = T_BLK.ID
LEFT JOIN V$SESSIONS S_BLK ON T_BLK.SESS_ID = S_BLK.SESS_ID
LEFT JOIN V$LOCK L ON L.TRX_ID = W.WAIT_FOR_ID AND L.LTYPE = 'TID'
LEFT JOIN SYSOBJECTS O ON L.TABLE_ID = O.ID ;

3.3 场景三:UPDATE/DELETE阻塞(修改已被锁记录)
若记录已被其他事务修改(未提交),当前事务修改同一记录会阻塞。
操作步骤如下:
sql
-- 会话 A,执行修改操作,不提交
UPDATE "t_test_lock" SET NAME = 'Frank' WHERE ID = 2;
-- 会话 B,修改操作阻塞
UPDATE "t_test_lock" SET NAME = 'Grace' WHERE ID = 2;

同理,查询阻塞会话链路

3.4 场景四:DML操作阻塞DDL
当一个事务正在进行 DML 操作,相关联对象无法执行 DDL 操作。
操作步骤如下:
sql
-- 会话 A
UPDATE t_test_lock SET NAME = 'Hank' WHERE ID = 1;
-- 会话 B,尝试删除表字段
ALTER TABLE t_test_lock DROP "NAME";

3.5 场景五:死锁
两个事务互相持有并请求对方锁。
操作步骤如下:
sql
-- 会话 A
UPDATE t_test_lock SET NAME = 'Hank' WHERE ID = 1; -- 锁住ID=1
UPDATE t_test_lock SET NAME = 'Ivy' WHERE ID = 2; -- 请求锁ID=2
-- 会话 B
UPDATE t_test_lock SET NAME = 'Jack' WHERE ID = 2; -- 锁住ID=2
UPDATE t_test_lock SET NAME = 'Kate' WHERE ID = 1; -- 请求锁ID=1

此时达梦数据库自动回滚事务,处理死锁。
4 常见锁等待解决办法
| 场景 | 触发条件 | 处理方式 |
|---|---|---|
| INSERT唯一冲突 | 插入相同主键值 | 提交/回滚先插入事务、预检查 |
| UPDATE/DELETE阻塞 | 修改已被锁记录 | 提交/回滚先修改事务 |
| 死锁 | 事务互相等待对方锁 | 优化访问顺序、缩短事务 |
锁查询 SQL:
sql
SELECT
W.ID AS 等待事务ID,
W.WAIT_FOR_ID AS 被等事务ID,
W.WAIT_TIME AS 等待时间_MS,
S_WAIT.SESS_ID AS 等待会话ID,
S_WAIT.USER_NAME AS 等待用户名,
S_WAIT.CLNT_IP AS 等待客户端IP,
S_WAIT.SQL_TEXT AS 等待SQL,
S_BLK.SESS_ID AS 阻塞会话ID,
S_BLK.USER_NAME AS 阻塞用户名,
S_BLK.CLNT_IP AS 阻塞客户端IP,
S_BLK.SQL_TEXT AS 阻塞SQL
FROM V$TRXWAIT W
LEFT JOIN V$TRX T_WAIT ON W.ID = T_WAIT.ID
LEFT JOIN V$SESSIONS S_WAIT ON T_WAIT.SESS_ID = S_WAIT.SESS_ID
LEFT JOIN V$TRX T_BLK ON W.WAIT_FOR_ID = T_BLK.ID
LEFT JOIN V$SESSIONS S_BLK ON T_BLK.SESS_ID = S_BLK.SESS_ID
LEFT JOIN V$LOCK L ON L.TRX_ID = W.WAIT_FOR_ID AND L.LTYPE = 'TID'
LEFT JOIN SYSOBJECTS O ON L.TABLE_ID = O.ID ;
KILL阻塞会话:
sql
-- session_id 为阻塞会话 ID
sp_close_session(session_id);
对于业务侧已确认某时间段无业务访问,可以直接进行DDL操作,但有时依旧报锁等待超时的错误,可以先对干扰维护的会话进行清理。
sql
SQL> SET SERVEROUT ON
SQL> declare
session_id bigint;
begin
select sess_id into session_id from v$sessions where sess_id<>sessid and UCASE(sql_text) like '%TEST%';
print session_id;
sp_close_session(session_id);
end
/