在日常运维中,经常会遇到一种现象:业务系统执行一个包含大量SQL的事务时,明明操作的数据量不大,却长时间没有响应,最终以超时或回滚告终。应用日志里只留下一句“操作超时”或“锁等待超时”,却看不出具体原因。
最近在一次运维排查中,就遇到了这样一个案例:某个事务在执行过程中被另一个事务阻塞,等待了约300000毫秒(约5分钟)后才最终回滚。阻塞方事务共执行了约10000条SQL,数量相当庞大。排查结论如下:
这个案例揭示了一个核心机制:一个事务内部的SQL不是并行执行的,而是串行执行的。它们的执行顺序与业务代码提交SQL到数据库的顺序一致。当两个事务修改同一条数据时,后执行的事务会被阻塞,必须等待先执行的事务释放锁。如果阻塞方事务执行了大量SQL、耗时很长,等待方就会一直等待,最终回滚。
下面我将用一个模拟案例来复现这个问题,并介绍达梦数据库中排查此类问题的常用工具和视图。
在达梦数据库中,一个事务内部的多条SQL语句并不是并行执行的,而是按照业务代码发送的顺序逐条串行执行。这意味着:
达梦数据库在第一次执行SQL语句时会隐式地启动一个事务,以COMMIT或ROLLBACK语句显式地结束事务。这意味着业务代码中如果在一个事务内批量执行了上万条SQL,这些SQL会全部串行执行,持有锁的时间也会相应延长。
达梦数据库采用行级锁机制,当两个事务修改同一条数据时,后执行的事务会被阻塞,必须等待先执行的事务释放锁。这是正常的并发控制机制——事务A修改一行数据但尚未提交,事务B修改同一行时等待A结束,这是预期的行为。
真正的问题在于:如果事务A迟迟不提交(因为它在执行大量SQL),事务B就会一直等待。如果等待时间超过了应用或数据库设定的超时阈值,事务B就会抛出锁超时错误并回滚。
这是很多运维人员最困惑的地方。原因在于:
所以,阻塞时间的长短取决于阻塞方事务的总执行时间,而不是等待方操作的数据量。
下面用两个disql会话模拟一个真实的阻塞场景。
创建测试用户和表:
CREATE USER LOCKDEMO IDENTIFIED BY "Lock2026@demo";
GRANT RESOURCE TO LOCKDEMO;
CREATE TABLE LOCKDEMO.T_ACCOUNT (
ACCOUNT_ID INT PRIMARY KEY,
ACCOUNT_NAME VARCHAR(50),
BALANCE DECIMAL(12,2),
UPDATE_TIME DATETIME
);
INSERT INTO LOCKDEMO.T_ACCOUNT VALUES (1001, '企业账户A', 500000.00, SYSDATE);
INSERT INTO LOCKDEMO.T_ACCOUNT VALUES (1002, '企业账户B', 300000.00, SYSDATE);
COMMIT;
会话A执行一个包含多条SQL的事务,但故意不提交:
UPDATE LOCKDEMO.T_ACCOUNT SET BALANCE = BALANCE - 1000, UPDATE_TIME = SYSDATE WHERE ACCOUNT_ID = 1001;
-- 模拟该事务继续执行大量其他SQL(此处用循环插入模拟)
BEGIN
FOR i IN 1..5000 LOOP
INSERT INTO LOCKDEMO.T_ACCOUNT VALUES (2000 + i, '临时记录' || i, 0, SYSDATE);
END LOOP;
COMMIT;
END;
/
在循环执行期间,会话A持有账户1001的排他锁,且事务未提交。
打开另一个disql会话,执行:
UPDATE LOCKDEMO.T_ACCOUNT SET BALANCE = BALANCE + 500 WHERE ACCOUNT_ID = 1001;
此时会话B会一直等待,直到会话A提交或回滚。
在第三个会话中,通过V$TRXWAIT和V$SESSIONS视图查询阻塞关系:
SELECT VTW.ID AS WAIT_TRX_ID,
VTW.WAIT_FOR_ID,
VTW.WAIT_TIME,
VS.SESS_ID,
VS.SQL_TEXT
FROM V$TRXWAIT VTW
LEFT JOIN V$TRX VT ON VTW.ID = VT.ID
LEFT JOIN V$SESSIONS VS ON VT.SESS_ID = VS.SESS_ID;
查询结果中,WAIT_FOR_ID表示正在执行的事务ID,WAIT_TIME表示已等待的毫秒数。
再查询被等待事务的具体信息:
SELECT VT.ID AS TRX_ID,
VS.SESS_ID,
VS.SQL_TEXT,
VS.APPNAME,
VS.CLNT_IP
FROM V$TRX VT
LEFT JOIN V$SESSIONS VS ON VT.SESS_ID = VS.SESS_ID
WHERE VT.ID = :wait_for_id;
大致的三个会话关系是:
当会话B等待时间超过数据库参数LOCK_WAIT_TIME(默认10秒)或应用设定的超时时间后,会话B会抛出锁超时错误并回滚。如果会话A执行了约10000条SQL需要数分钟,那么会话B就会等待数分钟后才回滚,这在业务上是不可接受的。
在打开监控开关(ENABLE_MONITOR=1)后,可以通过查询动态视图V$LONG_EXEC_SQLS来确定高负载的SQL语句。该视图显示最近1000条执行时间较长的SQL语句,默认记录超过1000毫秒的SQL。
SELECT * FROM V$LONG_EXEC_SQLS;
相关参数说明:
| 参数名 | 缺省值 | 说明 |
|---|---|---|
| ENABLE_MONITOR | 1 | 打开或关闭系统监控功能 |
| MONITOR_TIME | 1 | 打开或关闭时间监控 |
| LONG_EXEC_SQLS_CNT | 1000 | V$LONG_EXEC_SQLS的记录数上限 |
该视图适合快速发现执行时间异常的SQL语句,但记录的SQL文本可能被截断,适合做初步筛查。
V$SQL_HISTORY中记录数据库中的历史运行SQL,根据运行时间可以获取慢SQL语句信息。该表记录数由参数SQL_HISTORY_CNT指定,默认10000条。
SELECT TOP 10
TOP_SQL_TEXT,
TIME_USED,
START_TIME,
SESS_ID,
TRX_ID
FROM V$SQL_HISTORY
ORDER BY TIME_USED DESC;
该视图的优势在于记录了完整的SQL文本,并且可以关联SESS_ID和TRX_ID,方便定位是哪个会话、哪个事务执行的SQL。
如果需要记录所有SQL的执行情况(包括执行时间和参数信息),可以开启sqllog日志。在dm.ini中设置SVR_LOG=1,并配置sqllog.ini文件:
[SLOG_ALL]
FILE_PATH = /dm/data/sqllog
SWITCH_MODE = 2
SWITCH_LIMIT = 256
FILE_NUM = 20
ITEMS = 0
SQL_TRACE_MASK = 1
MIN_EXEC_TIME = 0
配置完成后,执行以下命令使配置生效:
SP_REFRESH_SVR_LOG_CONFIG();
生成的日志文件位于FILE_PATH指定的目录下,文件名格式为dmsql_实例名_日期_时间.log,其中记录了详细的SQL语句、执行时间、错误信息等。
这是排查阻塞问题最核心的组合:
-- 第一步:查询被挂起的事务
SELECT VTW.ID AS TRX_ID,
VS.SESS_ID,
VS.SQL_TEXT,
VS.APPNAME,
VS.CLNT_IP
FROM V$TRXWAIT VTW
LEFT JOIN V$TRX VT ON VTW.ID = VT.ID
LEFT JOIN V$SESSIONS VS ON VT.SESS_ID = VS.SESS_ID;
-- 第二步:通过挂起事务ID找到它等待的事务
SELECT WAIT_FOR_ID, WAIT_TIME
FROM V$TRXWAIT
WHERE ID = :trx_id;
-- 第三步:通过等待事务ID定位到连接以及执行的语句
SELECT VT.ID AS TRX_ID,
VS.SESS_ID,
VS.SQL_TEXT,
VS.APPNAME,
VS.CLNT_IP
FROM V$TRX VT
LEFT JOIN V$SESSIONS VS ON VT.SESS_ID = VS.SESS_ID
WHERE VT.ID = :wait_for_id;
通过这三步,可以完整地还原“谁在等谁、等了多久、等待方和被等待方分别在执行什么SQL”。
将包含大量SQL的事务拆分为多个小事务,减少单次事务的锁持有时间。例如,将10000条SQL的批量操作拆分为每500条提交一次:
BEGIN
FOR i IN 1..10000 LOOP
-- 执行业务SQL
IF MOD(i, 500) = 0 THEN
COMMIT;
END IF;
END LOOP;
COMMIT;
END;
/
如果多个事务需要更新同一批数据,统一加锁顺序可以避免死锁。例如,按主键升序处理批量更新,让所有事务排队走同一条路,环就永远闭不上。
根据业务容忍度,设置合理的LOCK_WAIT_TIME参数,避免事务无限期等待:
SP_SET_PARA_VALUE(1, 'LOCK_WAIT_TIME', 30);
对于热点账户、热点库存等数据,在应用层做队列化串行处理,避免多个事务同时争抢同一行锁。
定期查询V$SQL_HISTORY和V$LONG_EXEC_SQLS,发现执行时间较长的SQL并及时优化。
本次案例的核心结论可以归纳为三点:
排查此类问题的关键工具是V$TRXWAIT和V$SESSIONS视图,可以快速定位阻塞关系。日常运维中,建议开启ENABLE_MONITOR=1和sqllog日志,定期分析V$SQL_HISTORY和V$LONG_EXEC_SQLS,发现大事务和慢SQL及时优化。
文章
阅读量
获赞
