注册
事务与锁:用两个 disql 会话剖析 DM8 的事务与锁机制
培训园地/ 文章详情 /

事务与锁:用两个 disql 会话剖析 DM8 的事务与锁机制

DM_201195 2026/08/27 149 0 0

写在前面
并发是数据库最核心也最"玄"的话题。教科书会告诉你:行锁会阻塞、死锁会被检测、读已提交可能不可重复读——但这些结论背下来容易,真正理解很难,因为它们平时是看不见的:一条 UPDATE 执行完就返回了,谁也说不清那一瞬间锁表里发生了什么。
本文的思路是把并发现象"搬上手术台":开两个 disql 会话,让它们在同一张表上按脚本互相出手,再用系统视图实时围观。阻塞等待了多久、等待链长什么样、死锁牺牲了谁、隔离级别改变了什么,全部有图有数据。所有实验在 DM8(03134284604-20260707-335949-20228)上真实执行,读者照着做即可复现。
之所以坚持"双会话实测"而不是罗列结论,是因为并发行为强烈依赖时序:同一对语句,执行顺序差一秒,结果可能从"顺利提交"变成"互等成环"。只有把时间轴摆出来,每个现象的前因后果才站得住。这也是本文每个实验都附双窗口截图的原因——左侧窗口是会话 A 的完整时间线,右侧是会话 B 的,两个窗口对齐着看,锁的每一次出手与每一次等待都有据可查。
一、实验准备
实验表只有两行,够用且干净:

CREATE USER LOCKDEMO IDENTIFIED BY "Lock20260817adm"; GRANT RESOURCE TO LOCKDEMO; CREATE TABLE LOCKDEMO.t1 ( id INT PRIMARY KEY, val INT ); INSERT INTO LOCKDEMO.t1 VALUES(1, 100); INSERT INTO LOCKDEMO.t1 VALUES(2, 200); COMMIT;

实验方法:两个 disql 会话(下称会话 A、B)同时连接本实例,各自按时间脚本执行语句;每张截图的左右两个窗口分别是 A、B 的完整控制台输出,窗口左上角有标注。观察锁状态使用三个动态视图:

  • V$TRX:当前所有事务,重点看 ID(事务号)与 STATUS(ACTIVE 表示活跃);
  • V$LOCK:当前所有锁,LTYPE=TID 表示事务锁(每个活跃事务一把),LMODE=X 表示排他;BLOCKED=1 表示该锁上存在等待者;
  • V$TRXWAIT:等待关系图,一行记录一个"谁在等谁":ID 是等待方事务,WAIT_FOR_ID 是被等待方事务,WAIT_TIME 是已等待毫秒数。
    二、先备一点理论:锁模式与兼容矩阵
    DM8 的表级锁有四种常见模式:S(共享)、X(排他)、IS(意向共享)、IX(意向排他)。S/X 作用于数据对象本身,IS/IX 作用于表,用于表明"表中存在(将被)锁定的行"。它们之间的兼容关系如下图:
    image.png

图 1 表级锁模式兼容矩阵

看懂这张矩阵,实验里的现象就都能解释:两个事务分别 UPDATE 同一张表的不同行,各自先在表层加 IX——IX 与 IX 兼容,放行;到行级才真正互斥。所以"锁表"这种说法在 DM8 里几乎不会发生,冲突精确到行。
而普通 SELECT 根本不加表级 S 锁——DM8 与主流数据库一样采用多版本并发控制(MVCC),读操作走一致性快照。这一点在实验五里可以亲眼验证。
三、实验一:行锁阻塞,眼见为实
会话 A 更新行 1 但不提交,随后会话 B 更新同一行。按兼容矩阵推断:B 的 UPDATE 应该被 A 的行锁挡住。
image.png

图 2 行锁阻塞:A 持锁不提交,B 的 UPDATE 悬而未决

图 2 中,A 窗口显示 UPDATE 影响行数 1 后进入空闲(事务未提交,锁一直持有);B 窗口先打出标记 B_UPDATING_ROW1,随后发出 UPDATE——注意 B 的 UPDATE 语句和"影响行数"都没有出现,它正被阻塞。直到 25 秒后 A 执行 COMMIT,B 才继续:最终 B 的 UPDATE 耗时 13.3 秒(绝大部分是在等锁),并成功把行 1 改为自己的值。
一个小细节:B 的语句之所以在执行完成后才回显,是 disql 管道模式的行为——语句随结果一起输出,执行中则保持沉默。这个"沉默"恰好成了阻塞的可视化信号。
四、实验二:用 V$TRXWAIT 看等待链
阻塞发生时,第三个会话查询 V$TRXWAIT,把"谁在等谁"拍在案上:

image.png

图 3 三窗口观测:观察者用 V$TRXWAIT 拍下等待链

图 3 下方的观察者窗口里,V$TRXWAIT 返回了一行:等待方事务 7403,被等待方事务 7398,已等待 2531 毫秒——正是 B(7403)在等 A(7398)。随后的 V$TRX 查询里两个事务都处于 ACTIVE 状态:一个拿着锁慢慢干活,一个挂着号排队。
这套观察方法可以直接用于生产排障:某天应用突然大量超时,连上库先查 V$TRXWAIT,一眼就能定位"堵点"在哪两个事务之间,再用事务号关联 V$SESSIONS 找到持锁的会话与 SQL。
五、实验三:死锁的诞生与自动裁决
把交叉加锁的时序排出来:A 锁行 1,B 锁行 2,然后 A 去要行 2、B 去要行 1——互相持有对方想要的,等待链闭合成环,死锁成立。

image.png

图 4 死锁:B 被 DM 判定为牺牲方,报 -6403

图 4 的结果干净利落:B 的第二条 UPDATE 报出 [-6403]:死锁,从发出到报错只用了 617 毫秒——DM8 的死锁检测在等待形成环后的极短时间内就做出裁决,选择了 B 作为牺牲方,将其事务回滚以打破僵局。A 则毫发无损:它等行 2 的锁在 B 回滚后立即放行,两条 UPDATE 全部执行成功(A-done)。
生产环境看到 -6403 该怎么办?它是错误更是信号:说明存在两组以不同顺序访问同一批资源的语句。最经典的解法是统一加锁顺序——比如按主键升序处理批量更新,让所有事务排队走同一条路,环就永远闭不上。此外,把大事务拆小、在应用侧对热点账户做队列化串行,也都是行之有效的手段。若死锁只是偶发,数据库的自动裁决其实已经兜住了底:牺牲方事务回滚、另一方继续,业务层捕获该错误号做一次重试即可恢复,不必如临大敌。
六、实验四:同一张表,两种隔离级别
事务的隔离级别描述并发事务之间的可见性。DM8 支持三种,官方手册《DM8_SQL语言使用手册》第 9 章给出语法与语义:
"SET TRANSACTION ISOLATION LEVEL <事务隔离级>;<事务隔离级> ::= READ COMMITTED | READ UNCOMMITTED | SERIALIZABLE",其中"读提交(READ COMMITTED):DM 默认级别,保证不读脏数据"。
该语句必须在事务开始时执行。先看默认读已提交级别下的现象——A 两次读同一行,中间 B 修改并提交:

image.png

图 5 读已提交:A 两次读到不同值(不可重复读)

图 5 中 A 的第一次读得到 100;B 把值改成 777 并提交;A 的第二次读得到 777。同一个事务、同一行、两次结果不同——这就是不可重复读。对多数业务这可以接受(读到的是别人已提交的最新值),但对"先查后算再写"的核对型逻辑就是隐患。
换串行化级别再跑一遍。选择隔离级本质上是在"数据一致性视野"与"并发能力"之间做取舍:级别越严格,看到的并发世界越"静止",但事务冲突与回滚的概率也越高。理解取舍的最好方式不是背定义,而是让同一个场景在两个级别下各跑一遍,对比着看差异从哪里来——下面两组截图就是这样的对照实验:

image.png

图 6 串行化:A 两次读一致,B 的提交对其不可见

图 6 中 A 以 SERIALIZABLE 开启事务,两次读都是 100——尽管 B 早已把值改为 777 并提交。串行化让 A 的事务视角完全冻结,代价是并发度下降、且更易碰上更新冲突。日常业务用默认读已提交足够,核对类任务再按事务粒度切换串行化。
七、实验五:MVCC——写不阻塞读
前四个实验里"读"都是旁观者,最后让读站到灯下:A 更新行 1 不提交(持有行锁),B 去读同一行。

image.png

图 7 MVCC:B 在 A 持锁期间照常读到旧值 100

按锁的直觉,B 应该被 A 的 X 锁挡住;实际结果见图 7:B 的 SELECT 立刻返回,耗时 13 毫秒量级,读到的是 100——A 改成的 555 尚未提交,对 B 完全不可见。写没有阻塞读,读也没有读到脏数据,两头兼顾靠的就是 MVCC:UPDATE 把新版本留在别处,未提交前其他会话的读仍走旧版本;A 一旦 ROLLBACK,新版本作废, everybody 相安无事。
这也是 DM8(以及主流数据库)高并发的根基:读操作既不加锁也不等待,热点行上的大量读不会被一个写事务卡住。
八、踩坑记录
第一,disql 管道模式下语句在执行完成后才回显,实验编排时用一条轻量 SELECT(如 SELECT 888 AS 标记名)作为动作分隔符,阻塞中的沉默才有辨识度。
第二,SET TRANSACTION ISOLATION LEVEL 只对紧随其后的那个事务生效,且必须是事务第一条语句——放在事务中途执行会静默无效,这是复现隔离级别实验时最容易踩的空。
第三,双会话实验建议用两个窗口分别以不同连接串登录(localhost 与 127.0.0.1)。对 DM8 而言两者是同一实例,但 disql 会用连接串改写控制台标题,窗口天然可区分,截屏归档不会张冠李戴。
第四,观察 V$TRX 时会看到观察者自己的事务也位列其中(查询本身就是一个事务),解读时按 SESS_ID 排除自身即可。
九、总结
一张两行的表、两个会话、三个视图,把 DM8 并发机制的核心切面完整过了一遍:行锁让写写冲突精确到行并付出等待代价(B 等了 13 秒);V$TRXWAIT 把等待关系拍成等待链(7403 等 7398);死锁成环后 DM 在 617 毫秒内裁决并回滚牺牲方(-6403);读已提交看得见别人已提交的修改(100 变 777),串行化则冻结视角(两次都是 100);MVCC 让读写互不阻塞(持锁期间的读 13 毫秒返回旧值)。
这些数字本身不重要,重要的是"并发行为可以被观察、被测量、被解释"的方法:双会话编排 + 动态视图围观。这套方法不挑场景,换一张业务表、换两段真实业务的 SQL,就能用它审视你自己系统里的并发设计。
参考资料

  • 《DM8_SQL语言使用手册》第 9 章 一致性和并发性,达梦数据库官方手册
评论
后发表回复

作者

文章

阅读量

获赞

扫一扫
联系客服