资讯动态

达梦数据库阻塞会话排查实战:从sqllog定位锁等待到根治

发布时间:2026/10/17 6:25:33 来源:尧图企业网站定制
生产环境的数据库卡顿时最让人头疼的往往不是磁盘跑满或者CPU打满而是大量看起来活着的会话堆在锁等待上。去年有套基于达梦数据库的业务系统就出过这么一次状况某个接口的响应时间从几十毫秒涨到几十秒应用日志里全是锁超时的报错但登进数据库一看CPU和IO都很正常。最后把所有会话列表拉出来才发现问题出在一个最容易被忽略的地方——某个连接把一批订单数据更新了一半就停在事务里不提交后续所有相关行的更新请求只能在锁队列里排队。这篇文章就把当时在sqllog里排查阻塞会话的完整思路梳理一遍。标题里说的sqllog你可以理解成达梦环境里随手就能用的SQL查询与分析入口图形化管理工具也好、命令行交互窗口也好核心都是同一套方法先定位锁等待会话再反向找锁的持有者最后确认这个持有者是谁、在干什么、能不能安全处理。整个排查过程不需要什么高级工具几段动态视图查询就能搞定关键是思路要对别一上来就乱杀会话。这篇文章适合手里有达梦实例要维护的DBA、被线上锁问题折腾到焦头烂额的应用运维以及刚接触达梦想搞懂会话和锁机制的开发同学。1. 为什么要盯阻塞会话一次生产卡顿的现场回放1.1 现场症状连接池被占满SQL超时CPU却不忙先说那次具体的问题现场。当时业务方反馈的是订单保存偶发失败一开始大家以为是小问题结果半小时后失败率开始陡增后台监控里出现了一条又一条的会话被阻塞告警。我登到达梦服务器上先用系统命令看了一眼cpu和内存使用率都很低磁盘IO也平稳完全不像资源耗尽的样子。再进sqllog把当时的会话状态捞出来看一下就发现了异常有十几个会话的STATE是WAITWAIT_TIME从几秒到几百秒不等而且它们的SQL_TEXT高度相似都是对同一张订单表的UPDATE操作。这时候基本可以断定问题不是跑不动而是被堵住了。数据库本身还有大量空闲资源但应用端的连接全卡在等待锁上连接池很快就被这些等锁的请求占满了。新进来的请求拿不到连接表现就成了超时、失败、服务不可用。这个场景很有代表性——阻塞会话引发的故障症状往往在应用层原因却在数据库锁这一层。1.2 阻塞会话的影响锁等待只是开始连接池耗尽才是灾难很多人觉得锁等待就是SQL跑得慢一点忍一忍就过去了。实际不是这样。锁等待是会传导的持有锁的事务不提交等待的会话就一直WAIT等待的会话多了数据库连接池的连接全被占用新的业务请求拿不到连接应用线程也跟着阻塞应用线程堆积到一定程度直接把中间件和服务的线程池拖垮整个链路雪崩。更麻烦的是如果两个会话各自持有锁、又在等对方的锁那就从阻塞演变成了死锁。死锁至少数据库还能检测并回滚一方而纯阻塞不会自动解除它就一直卡在那直到持有事务提交、回滚或者被DBA强制关闭。数据库里的阻塞有时候就是这么隐蔽表面上一切正常只有把V$SESSIONS这类动态视图打开看到那一排WAIT状态你才会意识到问题的严重性。1.3 为什么锁等待不容易被常规监控发现常规监控一般都盯着CPU、内存、磁盘、网络很少有人会盯会话等待事件和锁等待队列长度。而阻塞会话恰恰是那种不占资源却能拖垮系统的故障类型。我后来养成了一个习惯不管什么问题排查数据库性能之前第一件事永远是看会话状态。CPU高不代表一定慢CPU不高也不代表没问题先把所有会话的STATE和WAIT_TIME过一遍心里就有底了。这种排查习惯配合sqllog会非常顺手。sqllog里可以直接执行动态视图查询也可以翻看历史SQL日志把当前队列和之前的表现对照起来看能很快缩小范围。2. 阻塞是怎么产生的达梦的锁与等待模型2.1 锁的粒度与模式行锁、表锁还有兼容性关系要排查阻塞先得理解达梦的锁机制。通俗点说锁就是数据库给正在被使用的数据立的牌子。别人发现牌子就明白这块数据有人动了想再动就得排队等牌子撤掉。达梦的锁可以分成几种粒度。最常见的是行锁比如你执行一条WHERE条件很精确的UPDATE会把命中的那一行锁住其他事务再想改这一行就要等。其次是表锁比如某些DDL操作、或者是全表扫描更新时锁的范围会放大。此外还有对象锁、事务锁等内部锁平时不需要管得太细但要知道它们的存在。锁的兼容性也很关键。打个比方一把共享锁像阅览室大家都进去看一把排他锁像单间一个人进去了别人就得在外边等。如果两个会话对同一行数据一个持排他锁、另一个也需要排他锁那后者就必须等待。排查阻塞时你不需要把锁模式背得滚瓜烂熟但至少要知道一点阻塞的本质就是我在等一个被别人拿走的锁而拿到锁的人还没松手。2.2 阻塞成立的两个条件一次典型的阻塞需要同时满足两个条件会话A持有某个资源的锁且事务一直没有提交或回滚会话B需要访问同一个资源且请求的锁模式与A持有的模式不兼容。A不动B就只能一直在锁队列里等着。如果A最终提交了等待自然解除如果A是个挂了连接但没断开的僵尸会话或者应用代码里事务忘记COMMITB可能就要等到数据库锁超时甚至永远等下去。我在生产库中碰到的阻塞绝大多数都不是什么复杂的并发设计问题往往就是一两条长事务没有及时结束。比如某开发同学在代码里写完UPDATE不COMMIT连接池又复用了这个连接后续所有碰同一行数据的请求全部原地排队。2.3 阻塞和死锁是两回事排查的时候千万不要把阻塞和死锁混为一谈。死锁是你等我、我也等你的循环等待数据库有专门的死锁检测机制通常会自动回滚其中一个事务报错信息里会有明显的dead lock字样。阻塞则是一个单向队列B等AC等BD等C整条链路都在等最前面的A松手但A没有任何等别人的迹象就是单纯地占着茅坑不动作。判断方法也很简单看V$SESSIONS里各个WAIT会话在等什么。如果所有等待都指向同一个源头那就是阻塞如果两个会话各自持有的资源恰好是对方正等待的资源那就是死锁。2.4 常见阻塞来源大多数时候都是人祸根据我的经验达梦数据库里的阻塞会话来源可以归结为几类未提交的长事务代码里忘写COMMIT或者事务里混了太耗时的外部调用大批量操作比如对几百万行数据UPDATE虽然行锁一个个加但事务持续很久期间所有碰这些行的请求都会等DDL与DML冲突有人在业务高峰执行了表结构变更DDL的锁会挡住所有DML应用连接复用问题事务没正常结束但连接被归还到连接池下个请求复用时还带着旧事务的尾巴。搞懂这些来源之后排查就有的放矢了不用漫无目的地翻会话。3. 从sqllog会话列表找到异常会话第一步排查记录3.1 打开sqllog先查会话视图sqllog窗口打开之后第一步永远是看一眼当前系统里都有哪些会话、处于什么状态。我习惯执行下面这条SQLSELECT SESS_ID, USER_NAME, STATE, TRX_ID, BLOCKED, WAIT_CLASS, WAIT_TIME, SQL_TEXT FROM V$SESSIONS WHERE STATE WAIT ORDER BY WAIT_TIME DESC;这条语句会把你最关心的东西都列出来会话ID、用户、会话状态、关联的事务ID、是否在等待、等待的时长、以及当前正在执行的那条SQL。字段大致含义如下字段含义说明SESS_ID会话ID后续关闭会话要用它STATE会话状态常见的有ACTIVE、WAIT、IDLE等TRX_ID当前的事务ID没结束的事务可以靠它追踪BLOCKED会话是否处于锁等待中1表示被阻塞WAIT_CLASS等待类型锁等待通常是相关锁/事务类标识WAIT_TIME已经等待的秒数越大越可疑SQL_TEXT会话正在执行的SQL文本如果你打开的sqllog版本字段略有差异也不用慌先执行一条SELECT * FROM V$SESSIONS WHERE ROWNUM 5;看看实际有哪些列再调整你的查询脚本。3.2 从结果里读出问题线索看到查询结果之后不要急着处理先做三个快速判断第一看WAIT_TIME的分布。等待时间从几秒到几百秒的梯度分布说明阻塞是持续累积的越靠前的会话等得越久越接近阻塞源头。第二看SQL_TEXT的内容相似度。好几个会话在等同一张表的UPDATE或者SELECT FOR UPDATE说明锁大概率落在这张表的某些行上。第三看USER_NAME和会话来源。如果等待会话全部来自同一个应用账号那基本就能锁定某个业务模块了。那次订单问题的排查结果就是十几条WAIT会话SQL全是针对订单表某几个状态字段的更新USER_NAME全部来自订单应用。看到这个结果的时候我心里已经有一个大概的判断了——肯定有人在订单表的某些行上持锁没放。3.3 把候选会话信息记下来别急着动很多人查到一堆WAIT会话第一反应就是杀掉它们——千万别这么干这些会话多半只是受害者你杀了它们源头还好好趴在那新请求一进来照样被堵。正确做法是先记录三组信息被阻塞会话的SESS_ID列表用于后续观察它们共同的等待对象这个要从V$LOCK中提取当前等待了多久、SQL是什么留着做问题复盘。记录完这些下一步就是顺着锁等待链往回找看看锁到底攥在谁手里。4. 顺着锁等待链追到根因V$LOCK与事务关联4.1 V$LOCK怎么读会话视图告诉我们谁在等锁视图则告诉我们等的是什么、被谁拿着。达梦的V$LOCK里最关键的一列就是BLOCKED值为1的那一行往往代表这条锁正在参与一次锁等待。实际排查时我会先执行SELECT * FROM V$LOCK WHERE BLOCKED 1;看哪些锁记录处于被阻塞状态把里面的TRX_ID、TABLE_ID记下来。TABLE_ID对应的是哪张表可以从达梦的视图里反查也可以用sqllog的图形化界面看对象详细信息这一步的目的就是弄清楚等在哪张表哪行数据上。4.2 反查阻塞源头找到了被阻塞的锁记录接下来的活儿就是找凶手。思路很简单同一个对象上如果有另一把锁处在未阻塞状态那它很可能就是阻塞源。实际执行时我常用类似的关联查询SELECT L1.SESS_ID AS WAITER_SESS_ID, L1.TRX_ID AS WAITER_TRX_ID, L2.SESS_ID AS BLOCKER_SESS_ID, L2.TRX_ID AS BLOCKER_TRX_ID, L2.SQL_TEXT FROM V$LOCK L1 LEFT JOIN V$SESSIONS S1 ON L1.TRX_ID S1.TRX_ID LEFT JOIN V$LOCK L2 ON L1.TABLE_ID L2.TABLE_ID AND L2.BLOCKED 0 WHERE L1.BLOCKED 1;需要说明的是不同版本的达梦在V$LOCK字段上会有些微差别这个查询不一定在所有版本上都能直接跑通但思路是通用的被阻塞的锁记录指向某个对象未阻塞的锁记录指向某个会话两边交集落在同一对象上时未阻塞的那一侧就是我们要找的源头。4.3 再从V$TRX确认事务状态V$LOCK找到的是锁最终持锁的却是事务。所以找到BLOCKER的TRX_ID之后还要拷问一下V$TRXSELECT ID, STATUS, CTIME, ISOLATION_LEVEL FROM V$TRX WHERE ID 业务里查到的那个TRX_ID;重点看STATUS是不是ACTIVE、CTIME代表运行了多久。如果这个事务已经ACTIVE了很长时间还没有结束十有八九就是它卡在那。再回到V$SESSIONS按TRX_ID查出这个事务对应的会话看一下这个会话当前的STATE和SQL_TEXT。那次排查我顺着这条链路查下去发现阻塞源头是一个STATE为IDLE的会话——也就是说这个会话没有在执行任何SQL但事务一直没提交。它的SQL_TEXT是空白的因为当前没跑语句但TRX_ID却还挂在那。说白了就是某段代码在事务中间异常退出既没COMMIT也没ROLLBACK把锁留了一地。4.4 用sqllog里的历史SQL日志还原因果锁视图看的是当下但很多时候需要知道之前发生了什么。sqllog的SQL日志功能这时候就派上用场了。去查看阻塞发生前一小段时间的SQL日志你会发现一条清晰的因果链阻塞源会话先执行了一个对某表的大范围UPDATE然后这条UPDATE之后的所有同类请求全部陆续进入WAIT状态。日志里SQL出现的时间戳会是很好的证据帮你确认到底是谁先起的头。我在复盘这类问题时一定把sqllog里的SQL日志导出来留档因为等一会儿处理完现场锁就释放了再想取证就没机会了。5. 决策与处理等、杀会话还是顺手补个索引5.1 先判断持锁方是活着干坏事还是死人占坑找到阻塞源头会话之后不要急着处理先判断它的状态STATE为ACTIVE且正在执行SQL可能是某个真实的大事务在跑要评估它的重要性STATE为IDLE但TRX_ID非空典型的占坑不拉屎多半是事务没结束就空闲下来的连接连接来源异常比如IP不对、端口不对可能是残留会话。判断清楚再决定动不动手。5.2 常规处理关闭阻塞源会话对于那种已经确认是异常残留的会话最直接的办法是把它关掉让它在数据库侧的事务回滚SP_CLOSE_SESSION(12345);这行语句会把会话ID为12345的连接强制关闭。会话一旦关闭它上面未提交的事务就会被回滚锁随之释放后面的等待队列自然全部解除。执行前有三个注意事项确认这个会话确实不是业务正在用的重要连接提前跟应用负责人打招呼关闭会话可能导致应用端报一个连接被终止的错执行完马上重新查一遍V$SESSIONS确认等待队列已经清空。那次订单问题最终就是用SP_CLOSE_SESSION解决的。找到那个IDLE但挂着事务的会话确认是应用层异常退出留下的孤儿连接之后一条命令关闭整个等待队列十几秒内全部消散应用恢复。5.3 不杀会话的情况怎么处理有些场景下你明知道持锁方是大事务但它在做正经事不能随便杀。这时候能做的选择有设置合理的锁等待超时时间让应用侧不要无限等下去跟业务方协商让那个大事务尽快提交如果大事务本身设计不合理考虑是否能用更小批量的方式分次处理。补充一句锁等待超时这种事配置在应用侧和数据库侧的关注点不同。数据库里可以关注锁等待的相关参数应用侧则会体现在SQL执行的超时设置。两边配合好了即使出现阻塞业务也不会被拖到连接池耗尽的程度。5.4 治本想想为什么会出现长时间持锁杀会话只是救火救完火还得想想火是怎么烧起来的。那次订单问题根子其实是两件事一是应用代码里有个事务边界没处理好。事务里除了数据库操作还夹了一段外部接口调用这段调用慢的时候整个事务就被拖住十几秒期间锁一直不释放。这属于典型的事务里做了不该做的事。二是那批订单数据在某个操作中出现了大范围更新。由于查询条件没有走索引数据库实际锁定的行数比业务预期的多得多连带的会话自然也多。针对这两点后续做了几件事把外部接口调用挪到事务外面给订单表的查询条件补了索引对大范围更新加了一个逐批提交的保护逻辑。锁问题从此消停了很多。6. 事后防线日常巡检与监控脚本沉淀6.1 巡检脚本要盯什么指标与其每次等故障爆发再冲到现场不如把排查逻辑变成定时巡检脚本。我在生产环境里挂了一套简单的轮询任务每隔一分钟执行一次核心就查一个东西——有多少会话处于锁等待状态SELECT COUNT(*) AS WAITING_CNT, MAX(WAIT_TIME) AS MAX_WAIT_SEC FROM V$SESSIONS WHERE STATE WAIT OR BLOCKED 1;正常情况下这个计数接近0。一旦超过阈值比如连续三次扫描发现仍有会话在WAIT就自动触发告警。这套逻辑在sqllog里手工执行也是一样的只不过用定时任务跑更有保障。6.2 把日志和视图结合起来看sqllog的SQL日志很有价值但平时没人会一直盯着它。建议在出现阻塞告警时同步做两件事取V$SESSIONS和V$LOCK的当前快照再导出告警前后几分钟的SQL日志。当前快照告诉你谁卡着谁历史日志告诉你为什么卡上两者结合才能写出像样的复盘报告。6.3 应用侧配合的几个硬要求数据库侧能做的监控只是最后一道防线真正减少阻塞还得靠应用侧的执行规范事务必须短平快开启事务后不要在里面做RPC调用、不要等用户输入同一条数据的高频更新尽量串行化或者引入状态机的设计避免多个流程同时抢同一行连接池配置里务必设置连接的最大生存时间和空闲回收防止孤儿连接长时间残留批量更新任务安排在低峰期并控制每批次的规模。这些都是老生常谈但每一条背后都有锁问题的血泪教训。那次订单故障如果应用侧早就遵守这些规范根本不需要DBA大半夜爬起来杀会话。根据我的经验达梦阻塞会话的排查思路可以浓缩成一句话先看V$SESSIONS找等待者再看V$LOCK找锁对象最后用事务和日志还原持锁者。sqllog这个入口足够完成整条链路的所有操作不需要额外装什么复杂的监控工具。最后分享一个小建议排查过程中每做一步操作前先截图或者保留查询记录。尤其杀掉会话之前一定要留好证据否则复盘的时候拿不出前因后果问题很容易在同一个月里再犯一次。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑