在数据库运维领域,锁阻塞问题一直是影响系统性能的关键瓶颈。达梦8作为国产数据库的代表产品,其锁机制与Oracle等传统数据库存在显著差异。这套"最终锁阻塞巡检SQL"脚本的诞生,源于我在某大型金融系统迁移项目中遇到的真实痛点——当业务高峰期出现性能骤降时,传统监控工具往往只能告诉我们"有锁等待",却无法快速定位到阻塞源头和受影响的关键业务。
这套脚本的价值在于:它不仅能实时捕获达梦8数据库中的锁阻塞链条,还能精确定位到阻塞会话的SQL语句、等待时长、涉及对象等关键信息。通过预设的阈值判断,运维人员可以快速区分"正常短时等待"和"异常长时阻塞",把平均故障定位时间从小时级缩短到分钟级。特别是在达梦8的共享服务器架构下,一个长时间持有锁的会话可能拖垮整个实例,这种巡检工具的重要性更是不言而喻。
达梦8采用多粒度锁机制,包含表级锁(TM锁)和行级锁(TX锁)。与Oracle不同的是,其锁转换规则更为严格:
特别需要注意的是达梦8的"锁继承"特性:当会话A阻塞会话B,而会话B又阻塞会话C时,会形成级联阻塞链。传统查询只能看到A→B的关系,而我们的脚本通过递归CTE可以追踪整个链条。
核心数据源来自达梦8的V$LOCK和V$SESSION视图,但需要特别注意:
sql复制SELECT * FROM V$LOCK WHERE BLOCK = 1; -- 仅显示主动阻塞的锁
SELECT * FROM V$SESSION WHERE BLOCKING_SESSION IS NOT NULL; -- 被阻塞会话
达梦8的锁等待超时参数(DM.INI中的LOCK_TIMEOUT)默认为-1(无限等待),这使长时阻塞更容易发生。我们的脚本会特别标注超过300秒的等待,这通常是需要立即干预的临界值。
sql复制WITH BLOCK_CHAIN AS (
-- 基础阻塞对查询
SELECT
HOLDER.SID AS HOLDER_SID,
WAITER.SID AS WAITER_SID,
HOLDER.USERNAME AS HOLDER_USER,
WAITER.USERNAME AS WAITER_USER,
HOLDER.STATUS AS HOLDER_STATUS,
WAITER.EVENT AS WAITER_EVENT,
HOLDER.MACHINE AS HOLDER_MACHINE,
WAITER.SECONDS_IN_WAIT
FROM
V$SESSION HOLDER,
V$SESSION WAITER
WHERE
WAITER.BLOCKING_SESSION = HOLDER.SID
UNION ALL
-- 递归查询阻塞链
SELECT
CHAIN.HOLDER_SID,
WAITER.SID,
CHAIN.HOLDER_USER,
WAITER.USERNAME,
CHAIN.HOLDER_STATUS,
WAITER.EVENT,
CHAIN.HOLDER_MACHINE,
WAITER.SECONDS_IN_WAIT
FROM
BLOCK_CHAIN CHAIN,
V$SESSION WAITER
WHERE
WAITER.BLOCKING_SESSION = CHAIN.WAITER_SID
)
SELECT * FROM BLOCK_CHAIN
ORDER BY HOLDER_SID, SECONDS_IN_WAIT DESC;
sql复制SELECT
CHAIN.*,
HOLDER_SQL.SQL_TEXT AS HOLDER_SQL,
WAITER_SQL.SQL_TEXT AS WAITER_SQL
FROM
BLOCK_CHAIN CHAIN
LEFT JOIN V$SQLAREA HOLDER_SQL ON HOLDER_SQL.ADDRESS = (
SELECT SQL_ADDRESS FROM V$SESSION WHERE SID = CHAIN.HOLDER_SID
)
LEFT JOIN V$SQLAREA WAITER_SQL ON WAITER_SQL.ADDRESS = (
SELECT SQL_ADDRESS FROM V$SESSION WHERE SID = CHAIN.WAITER_SID
)
sql复制SELECT
OBJECT_NAME,
SESSION_ID
FROM
V$LOCKED_OBJECT LO,
DBA_OBJECTS OBJ
WHERE
LO.OBJECT_ID = OBJ.OBJECT_ID
sql复制CASE
WHEN SECONDS_IN_WAIT < 60 THEN '轻微(<1min)'
WHEN SECONDS_IN_WAIT < 300 THEN '警告(1-5min)'
ELSE '严重(>5min)'
END AS WAIT_LEVEL
建议通过达梦8的作业系统设置每5分钟执行一次:
sql复制BEGIN
DBMS_JOB.SUBMIT(
JOB => :job_no,
WHAT => 'BEGIN
INSERT INTO BLOCK_HISTORY
SELECT *, SYSDATE FROM DM_BLOCK_CHECK_V;
END;',
NEXT_DATE => SYSDATE,
INTERVAL => 'SYSDATE + 5/1440'
);
COMMIT;
END;
历史记录表结构示例:
sql复制CREATE TABLE BLOCK_HISTORY (
DETECT_TIME TIMESTAMP,
HOLDER_SID NUMBER,
WAITER_SID NUMBER,
HOLDER_USER VARCHAR(30),
WAITER_USER VARCHAR(30),
HOLDER_SQL CLOB,
WAITER_SQL CLOB,
OBJECT_NAME VARCHAR(128),
WAIT_SECONDS NUMBER,
WAIT_LEVEL VARCHAR(20)
);
推荐报警规则组合:
对应的报警SQL:
sql复制SELECT * FROM (
SELECT
HOLDER_SID,
COUNT(*) AS BLOCK_COUNT
FROM
BLOCK_CHAIN
GROUP BY
HOLDER_SID
) WHERE BLOCK_COUNT >= 3;
SELECT * FROM BLOCK_CHAIN WHERE SECONDS_IN_WAIT > 300;
SELECT * FROM BLOCK_CHAIN
WHERE OBJECT_NAME IN ('ACCOUNT_INFO', 'TRADE_RECORD', 'PAYMENT_ORDER');
DDL阻塞DML:
外键锁升级:
事务未提交:
sql复制SELECT /*+ FIRST_ROWS(10) */ SQL_TEXT FROM V$SQLAREA WHERE ROWNUM < 10
sql复制CREATE TABLE BLOCK_HISTORY (
DETECT_TIME TIMESTAMP,
...
) PARTITION BY RANGE (DETECT_TIME) (
PARTITION P202301 VALUES LESS THAN (TO_DATE('2023-02-01','YYYY-MM-DD')),
PARTITION P202302 VALUES LESS THAN (TO_DATE('2023-03-01','YYYY-MM-DD'))
);
sql复制CREATE MATERIALIZED VIEW MV_BLOCK_STATS
REFRESH COMPLETE ON DEMAND
AS
SELECT
TRUNC(DETECT_TIME) AS DAY,
HOLDER_USER,
COUNT(*) AS TOTAL_BLOCKS
FROM
BLOCK_HISTORY
GROUP BY
TRUNC(DETECT_TIME), HOLDER_USER;
通过达梦8的UTL_HTTP包直接将报警推送到监控平台:
sql复制BEGIN
UTL_HTTP.SET_PROXY('proxy.example.com:8080');
UTL_HTTP.REQUEST(
url => 'http://monitor/api/alert',
method => 'POST',
body => JSON_OBJECT(
'db_host' VALUE SYS_CONTEXT('USERENV','SERVER_HOST'),
'block_sid' VALUE :holder_sid,
'wait_seconds' VALUE :wait_seconds
)
);
END;
使用达梦8的分析函数计算阻塞趋势:
sql复制SELECT
TRUNC(DETECT_TIME, 'HH24') AS HOUR,
COUNT(*) AS TOTAL_BLOCKS,
ROUND(AVG(SECONDS_IN_WAIT)) AS AVG_WAIT,
MAX(SECONDS_IN_WAIT) AS MAX_WAIT
FROM
BLOCK_HISTORY
WHERE
DETECT_TIME > SYSDATE - 7
GROUP BY
TRUNC(DETECT_TIME, 'HH24')
ORDER BY
HOUR DESC;
对于已知的安全会话,可以配置自动解锁脚本:
sql复制DECLARE
CURSOR BLOCK_CURSOR IS
SELECT HOLDER_SID
FROM BLOCK_CHAIN
WHERE SECONDS_IN_WAIT > 600
AND HOLDER_USER = 'BATCH_USER';
BEGIN
FOR R IN BLOCK_CURSOR LOOP
EXECUTE IMMEDIATE 'ALTER SYSTEM KILL SESSION ''' || R.HOLDER_SID || ''' IMMEDIATE';
INSERT INTO KILL_LOG VALUES(SYSDATE, R.HOLDER_SID, 'AUTO_KILL');
END LOOP;
COMMIT;
END;