注册
达梦数据库SQL堵塞排查实战:一个事务阻塞导致回滚的案例分析
专栏/培训园地/ 文章详情 /

达梦数据库SQL堵塞排查实战:一个事务阻塞导致回滚的案例分析

何处惹尘埃 2026/09/13 118 0 0
摘要

一、问题背景

在日常运维中,经常会遇到一种现象:业务系统执行一个包含大量SQL的事务时,明明操作的数据量不大,却长时间没有响应,最终以超时或回滚告终。应用日志里只留下一句“操作超时”或“锁等待超时”,却看不出具体原因。

最近在一次运维排查中,就遇到了这样一个案例:某个事务在执行过程中被另一个事务阻塞,等待了约300000毫秒(约5分钟)后才最终回滚。阻塞方事务共执行了约10000条SQL,数量相当庞大。排查结论如下:

  • 事务A被事务B所阻塞,等待约300000毫秒后才继续执行并最终回滚;
  • 两个事务修改了同一条数据,导致锁冲突;
  • 阻塞方事务B共执行了约10000个SQL,执行时间过长,是问题的根源。

这个案例揭示了一个核心机制:一个事务内部的SQL不是并行执行的,而是串行执行的。它们的执行顺序与业务代码提交SQL到数据库的顺序一致。当两个事务修改同一条数据时,后执行的事务会被阻塞,必须等待先执行的事务释放锁。如果阻塞方事务执行了大量SQL、耗时很长,等待方就会一直等待,最终回滚。

下面我将用一个模拟案例来复现这个问题,并介绍达梦数据库中排查此类问题的常用工具和视图。

二、核心原理:事务串行执行与锁阻塞

2.1 事务内的SQL是串行执行的

在达梦数据库中,一个事务内部的多条SQL语句并不是并行执行的,而是按照业务代码发送的顺序逐条串行执行。这意味着:

  • 如果事务A包含100条UPDATE语句,它们会按顺序依次执行,前一条提交完成后下一条才开始;
  • 这100条SQL的执行时间之和,就是事务A从开始到提交的总耗时;
  • 在事务A执行期间,它持有的锁会一直保持,直到事务提交或回滚才释放。

达梦数据库在第一次执行SQL语句时会隐式地启动一个事务,以COMMIT或ROLLBACK语句显式地结束事务。这意味着业务代码中如果在一个事务内批量执行了上万条SQL,这些SQL会全部串行执行,持有锁的时间也会相应延长。

2.2 两个事务修改同一条数据导致阻塞

达梦数据库采用行级锁机制,当两个事务修改同一条数据时,后执行的事务会被阻塞,必须等待先执行的事务释放锁。这是正常的并发控制机制——事务A修改一行数据但尚未提交,事务B修改同一行时等待A结束,这是预期的行为。

真正的问题在于:如果事务A迟迟不提交(因为它在执行大量SQL),事务B就会一直等待。如果等待时间超过了应用或数据库设定的超时阈值,事务B就会抛出锁超时错误并回滚。

2.3 为什么“没几条数据”却要等很久?

这是很多运维人员最困惑的地方。原因在于:

  • 阻塞方事务可能持有一个热点数据的排他锁(比如某个账户余额、某个库存记录),等待方只需要修改这一条数据就会被阻塞;
  • 但阻塞方事务本身可能在执行大量其他SQL(比如批量处理了10000条记录),这些SQL全部执行完才会提交、释放锁;
  • 等待方虽然只操作了一条数据,但必须等到阻塞方整个事务提交后才能继续。

所以,阻塞时间的长短取决于阻塞方事务的总执行时间,而不是等待方操作的数据量。

三、模拟案例:双会话复现阻塞场景

下面用两个disql会话模拟一个真实的阻塞场景。

3.1 环境准备

创建测试用户和表:

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;

3.2 会话A:模拟持有大量SQL的阻塞事务

会话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的排他锁,且事务未提交。

3.3 会话B:模拟被阻塞的事务

打开另一个disql会话,执行:

UPDATE LOCKDEMO.T_ACCOUNT SET BALANCE = BALANCE + 500 WHERE ACCOUNT_ID = 1001;

此时会话B会一直等待,直到会话A提交或回滚。

3.4 查询阻塞关系

在第三个会话中,通过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;

大致的三个会话关系是:
截屏20260911 16.01.32.png

3.5 阻塞的危害

当会话B等待时间超过数据库参数LOCK_WAIT_TIME(默认10秒)或应用设定的超时时间后,会话B会抛出锁超时错误并回滚。如果会话A执行了约10000条SQL需要数分钟,那么会话B就会等待数分钟后才回滚,这在业务上是不可接受的。

四、排查工具与视图详解

4.1 V$LONG_EXEC_SQLS:查询执行时间较长的SQL

在打开监控开关(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文本可能被截断,适合做初步筛查。

4.2 V$SQL_HISTORY:查询历史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。

4.3 sqllog日志:完整的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语句、执行时间、错误信息等。

4.4 V$ TRXWAIT + V$SESSIONS:定位阻塞源头

这是排查阻塞问题最核心的组合:

-- 第一步:查询被挂起的事务 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”。

五、优化建议

5.1 拆分大事务

将包含大量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; /

5.2 统一加锁顺序

如果多个事务需要更新同一批数据,统一加锁顺序可以避免死锁。例如,按主键升序处理批量更新,让所有事务排队走同一条路,环就永远闭不上。

5.3 设置合理的锁等待超时

根据业务容忍度,设置合理的LOCK_WAIT_TIME参数,避免事务无限期等待:

SP_SET_PARA_VALUE(1, 'LOCK_WAIT_TIME', 30);

5.4 热点数据串行化处理

对于热点账户、热点库存等数据,在应用层做队列化串行处理,避免多个事务同时争抢同一行锁。

5.5 定期分析慢SQL

定期查询V$SQL_HISTORY和V$LONG_EXEC_SQLS,发现执行时间较长的SQL并及时优化。

六、总结

本次案例的核心结论可以归纳为三点:

  1. 一个事务内部的SQL是串行执行的,执行顺序与业务代码提交SQL的顺序一致,事务总耗时等于所有SQL执行时间之和;
  2. 两个事务修改同一条数据会导致阻塞,后执行的事务必须等待先执行的事务提交或回滚才能继续;
  3. 阻塞时间取决于阻塞方事务的总执行时间,而不是等待方操作的数据量。即使等待方只操作一条数据,如果阻塞方事务执行了约10000条SQL,等待方也要等这么久。

排查此类问题的关键工具是V$TRXWAIT和V$SESSIONS视图,可以快速定位阻塞关系。日常运维中,建议开启ENABLE_MONITOR=1和sqllog日志,定期分析V$SQL_HISTORY和V$LONG_EXEC_SQLS,发现大事务和慢SQL及时优化。

评论
后发表回复

作者

文章

阅读量

获赞

扫一扫
联系客服