最近在梳理DM9并发更新问题时,我碰到一个很典型的现象:业务线程执行一条UPDATE后迟迟不返回,应用日志最后只留下一个超时。第一反应很容易往慢SQL上靠,但真正排查时,两者的方向并不一样。慢SQL通常要看执行计划、扫描行数和I/O;锁等待首先要回答的是:这条SQL是不是正在等另一个事务?谁在等?又是谁一直没有提交?
为了把这个现象说清楚,我单独做了一组双会话实验:会话A更新商品库存后保持事务不提交,会话B再去更新同一条记录,同时采集等待关系、锁明细和JDBC超时结果。这样变量少一些,问题也更容易复现,不会被复杂业务逻辑干扰。
这次我只验证三个问题:
这里需要先分清一个概念。锁等待本身不一定是故障。事务A修改一行数据但尚未提交,事务B修改同一行时等待A结束,这是正常的并发控制。真正需要排查的是等待时间过长,并且已经超过应用、连接池或数据库允许的时间范围。
本次测试使用DM9 x86 CentOS 7安装介质,实际运行环境是Ubuntu 24.04隔离容器。数据库程序能够启动,JDBC可以连接并完成事务测试,不过这只能证明本次功能验证可运行,不能据此替代正式兼容性认证。生产部署时,安装包、操作系统和CPU架构仍应按照官方适配范围选择。
| 项目 | 本次实际环境 |
|---|---|
| DM9服务端 | DM Database Server 64 V9;03151060506-20260417-322930-20218;DB Version 0x7000D |
| JDBC驱动 | DmJdbcDriver11 9.1.0.26 |
| 操作系统 | Ubuntu 24.04.3 LTS;Linux 6.18.35;x86_64 |
| CPU | 9 vCPU;AMD EPYC 9V74 80-Core Processor;KVM |
| 内存 | 15 GiB;无Swap |
| 磁盘 | 32 GiB虚拟磁盘;系统标记ROTA=1 |
| Java | OpenJDK 17.0.20 |
| 实例参数 | 端口5236;页大小16K;BUFFER 2048 MB;WORKER_THREADS 16;TASK_THREADS 16 |
| 测试程序 | 自编Java JDBC双会话并发程序 |
安装包由5个分卷重新合并,ZIP的SHA256为:
348cb2295b0c6f205275473eff07c7b7a0986d3ac95a6e037b0d49fede157a1b
该值与上传时保存的校验文件一致。测试容器对后台进程和网络命名空间有额外限制,因此数据库服务与JDBC程序放在同一个命令会话中运行。这个限制不会改变两条数据库连接之间的事务关系,但文章中的时间数据仍然只代表本次环境。
为了把问题收窄,我新建了LOCK_PRODUCT表,只保留商品ID、库存、版本号和更新时间。这里验证的是两个事务命中同一条记录后的锁冲突,大数据量反而容易增加无关变量,用两条可控记录更方便观察事务行为。
CREATE TABLE LOCK_PRODUCT (
PRODUCT_ID INT PRIMARY KEY,
PRODUCT_NAME VARCHAR(64) NOT NULL,
STOCK INT NOT NULL,
VERSION_NO INT DEFAULT 0 NOT NULL,
UPDATED_AT TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);
INSERT INTO LOCK_PRODUCT
VALUES (1001, 'DM9技术手册', 1000, 0, CURRENT_TIMESTAMP);
INSERT INTO LOCK_PRODUCT
VALUES (1002, '数据库课程', 1000, 0, CURRENT_TIMESTAMP);
COMMIT;
两条数据库连接都关闭自动提交。会话A先更新PRODUCT_ID=1001,但暂时不提交;会话B随后更新同一行。测试程序使用System.nanoTime()记录会话B从提交SQL到返回之间的时间,避免把建连和初始化耗时算进去。
正式测试前,我把每一轮的库存重置为1000、版本号重置为0,并单独提交。这样上一轮事务不会影响下一轮结果。
第一组测试不设置短超时,目的不是制造报错,而是确认正常锁等待的行为。
会话A执行下面的更新后保持事务打开:
UPDATE LOCK_PRODUCT
SET STOCK = STOCK - 1,
VERSION_NO = VERSION_NO + 1,
UPDATED_AT = CURRENT_TIMESTAMP
WHERE PRODUCT_ID = 1001;
-- 此处暂不提交
会话B执行完全相同的SQL。程序等待1500毫秒后提交会话A,再观察会话B何时返回。
最终一轮的三次结果如下:
| 轮次 | 会话A保持时间 | 会话B实际耗时 | 执行结果 | 最终库存 | 最终版本号 |
|---|---|---|---|---|---|
| 1 | 1500 ms | 1501 ms | 成功 | 998 | 2 |
| 2 | 1500 ms | 1501 ms | 成功 | 998 | 2 |
| 3 | 1500 ms | 1502 ms | 成功 | 998 | 2 |
会话B的耗时基本贴着会话A的持锁时间走,中位数为1501毫秒。会话A一提交,会话B就继续完成更新。库存从1000变成998,版本号从0变成2,说明两次更新都提交成功,并没有发生丢失更新。
这个结果也说明,看到UPDATE停在那里时,不能马上把它定义成慢SQL。SQL执行计划可能没有问题,它只是还没有拿到需要的锁。
第二组测试让会话A继续持有同一行,会话B进入等待。等待开始约700毫秒后,我用第三条管理连接查询V$TRXWAIT:
SELECT ID AS WAIT_TRX_ID,
WAIT_FOR_ID AS BLOCK_TRX_ID
FROM V$TRXWAIT;
最终一轮采集到的结果是:
WAIT_TRX_ID BLOCK_TRX_ID
48113 48112
这条结果已经把关系说得很清楚:事务48113正在等待事务48112。事务编号是运行时产生的,每次执行都会变化,所以现场排查必须查询当时的动态视图,不能照搬文章里的编号。
接着,把两个事务对应的锁记录查出来:
SELECT TRX_ID, TABLE_ID, LTYPE, LMODE, BLOCKED
FROM V$LOCK
WHERE TRX_ID IN (
SELECT ID FROM V$TRXWAIT
UNION
SELECT WAIT_FOR_ID FROM V$TRXWAIT
);
结果中同时出现了OBJECT / IX和TID / X记录,其中事务48113的一条TID锁记录为LMODE=X、BLOCKED=1。结合V$TRXWAIT,可以确认它不是单纯执行时间长,而是在等待事务48112释放冲突资源。
实际排查时,我会先看V$TRXWAIT确定等待方向,再看V$LOCK确认锁类型和阻塞状态。两个视图一起使用,比只盯着应用日志里的“请求超时”更直接。
这一部分有个细节需要单独说明。
我先查询了本实例V$DM_INI中名称包含WAIT的参数,共返回18项,其中有DDL_WAIT_TIME、BLDR_WAIT_TIME等,但本次DM9版本没有返回LOCK_WAIT_TIMEOUT或TRX_WAIT_TIME。因此我没有照搬网上其他版本的参数修改命令,而是在会话B的JDBC语句上设置3秒超时:
try (PreparedStatement ps = conn.prepareStatement(updateSql)) {
ps.setQueryTimeout(3);
ps.setInt(1, 1001);
ps.executeUpdate();
}
完整程序独立运行4次,超时结果如下:
| 运行批次 | 设置值 | 实际返回时间 | 错误码 | SQLState | 错误摘要 |
|---|---|---|---|---|---|
| 1 | 3 s | 4028 ms | -608 | 22000 | session request timeout |
| 2 | 3 s | 4028 ms | -608 | 22000 | session request timeout |
| 3 | 3 s | 4008 ms | -608 | 22000 | session request timeout |
| 4 | 3 s | 4018 ms | -608 | 22000 | session request timeout |
实际返回时间稳定在4秒左右,比setQueryTimeout(3)多约1秒。这里的3秒是JDBC语句层设置值,4018毫秒是从executeUpdate()开始到异常返回的端到端时间。驱动发起取消、服务端处理请求以及程序重新获得执行权都可能带来额外耗时,所以不能把设置值理解成精确到毫秒的截止点。
更重要的是,这次收到的-608 session request timeout属于会话请求超时。根因是否为锁等待,不能只靠这一行错误判断。本文能够确认锁等待,是因为同一时间的V$TRXWAIT记录了48113等待48112,V$LOCK也记录了TID / X / BLOCKED=1。错误日志和数据库现场证据能够互相对应,结论才算站得住。
超时发生后,程序先回滚会话B,再回滚会话A,没有把失败事务继续留在连接上。这个处理很关键。连接池如果把异常后的连接直接归还,而事务状态又没有清干净,后面的请求会更难查。
只把超时时间调大,通常解决不了持锁事务过长的问题。应用层更应该控制事务边界和数据访问顺序。
对照测试中,每轮同时启动两个事务,每个事务都先更新1001,再更新1002,中间保留30毫秒处理时间,最后提交:
int[] productIds = {1001, 1002};
Arrays.sort(productIds);
for (int productId : productIds) {
updateStock(conn, productId);
}
conn.commit();
两个事务仍然可能短暂等待,因为它们都会先访问1001,但锁定顺序保持一致后,不会人为形成“事务A拿着1001等1002、事务B拿着1002等1001”的交叉路径。
同一套程序独立运行4次,每次20轮、每轮2个事务,共执行160个并发事务:
| 运行批次 | 事务数 | 成功 | 失败 | 平均耗时 | P95 |
|---|---|---|---|---|---|
| 1 | 40 | 40 | 0 | 48.750 ms | 66 ms |
| 2 | 40 | 40 | 0 | 47.675 ms | 65 ms |
| 3 | 40 | 40 | 0 | 47.550 ms | 64 ms |
| 4 | 40 | 40 | 0 | 47.800 ms | 65 ms |
四次运行全部完成,失败数为0。最终一轮结束后,两条记录的库存都从1000变成960,版本号都变成40,与40个成功事务完全对应。
我不会据此写成“固定顺序可以消除所有锁等待”。它能减少交叉锁序,但不能让同一条热点记录变成无竞争状态。数据分布、事务长度、索引变化和并发规模都会影响结果。准确一点的表述是:在本次两行、双事务、固定顺序的对照条件下,160个事务全部成功,没有出现请求超时。
根据这次测试,我把现场排查顺序整理成下面几步。
至少记录错误发生时间、数据库地址、业务请求ID、SQL摘要、绑定主键、JDBC错误码、SQLState和实际耗时。只留一句“接口超时”,后面很难与数据库会话对应。
还要确认超时来自哪一层。HTTP网关、线程池、连接池、JDBC语句和数据库内部等待,都可能有自己的时间限制。它们虽然都叫“超时”,含义并不一样。
业务连接已经阻塞时,不要继续用同一条连接执行诊断SQL。应使用单独的管理连接查询V$TRXWAIT和V$LOCK,先得到等待事务与阻塞事务的关系,再结合会话信息定位来源。
如果查询时阻塞已经消失,动态视图可能为空,因此采集动作要尽量靠近故障时间。生产环境最好把关键等待信息纳入定时监控,而不是每次都等人手工登录。
常见问题不是单条UPDATE写得多复杂,而是事务里夹了别的工作,例如远程接口、文件上传、消息发送、循环计算,甚至等待人工确认。数据库更新已经完成,但提交要等到整段业务结束,锁自然也被一起拖长。
我个人更倾向于先改事务边界:把与数据库一致性无关的耗时操作移出事务,更新完成后尽快提交;异常路径必须回滚;批量任务按可控批次提交。这样做通常比单纯把超时时间从3秒改成30秒更有效。
一次事务需要修改多条记录时,可以先对主键排序,再按固定顺序更新。订单扣减多个商品库存、账户间转账、批量修改任务状态,都适合先检查访问顺序是否一致。
重试可以处理少量瞬时冲突,但必须满足几个条件:当前事务已经完整回滚;操作具备幂等性;重试次数有限;两次重试之间有退避时间;日志能够关联原请求。没有这些前提,直接重试只会把热点行压得更重。
线上需要结束阻塞会话时,还要先确认事务归属和影响范围,不能只因为它排在阻塞链前面就直接处理。未提交事务可能包含已经完成但尚未持久化的业务操作,贸然中断会带来数据和业务风险。
这次把并发更新单独拆出来后,几个现象比较明确。
第一,同一行更新发生冲突时,后执行的事务会等待持锁事务结束。会话A保持1500毫秒,三次会话B耗时分别为1501、1501、1502毫秒;会话A提交后,两次更新均正常完成。
第二,应用收到请求超时,并不等于已经知道了数据库根因。本次JDBC设置3秒后,实际约4秒返回-608,而V$TRXWAIT和V$LOCK才是确认锁等待的直接证据。
第三,固定更新顺序不能消除热点竞争,但能够避免程序自己制造交叉访问顺序。本次4批共160个并发事务全部成功,平均耗时保持在47.550~48.750毫秒,P95为64~66毫秒。
最后还有一个版本细节值得记录:本次DM9实例没有在V$DM_INI中查到LOCK_WAIT_TIMEOUT或TRX_WAIT_TIME,所以本文没有给出未经验证的数据库参数修改命令。遇到类似问题时,参数名称和作用范围应以实际版本查询结果及对应官方文档为准。
说到底,锁等待排查不是看到“超时”就开始调参数。先把等待链找出来,确认谁持锁、事务为什么没有结束,再决定是调整事务边界、统一访问顺序、增加监控,还是设置有限重试。顺序对了,问题通常会清楚很多。
#达梦数据库 #达梦同行者征文
文章
阅读量
获赞
