GBase 8c锁冲突与死锁排查实战:从原理到命令全解析
发布时间:2026/9/13 12:32:38 锦皓数字建站

GBase 8c 锁冲突与死锁排查实战解析数据库跑着跑着突然卡住业务侧一堆超时告警应用日志里全是锁等待的报错——做过数据库运维的朋友对这个场景一定不陌生。尤其在使用 GBase 8c 这类国产分布式数据库时锁冲突和死锁一旦出现如果不清楚底层原理排查起来就像无头苍蝇只能靠重启实例碰运气。这篇文章我结合自己多次处理线上问题的经验把 GBase 8c 的锁冲突与死锁排查思路、实战命令、典型案例一次性讲透。GBase 8c 作为一款融合了 shared-nothing 架构和多核处理器优势的分布式数据库在金融、政务等对数据一致性要求极高的场景里用得越来越广。它支持行级锁、表级锁并且基于 MVCC多版本并发控制机制来处理读写并发但这并不意味着你可以高枕无忧——长时间未提交的事务、不合理的索引设计、应用代码里的跨会话交叉更新都会把系统拖进锁等待甚至死锁的泥潭。这篇文章适合刚接手 GBase 8c 维护的 DBA、正在排查业务卡顿的运维人员以及想把数据库并发控制原理彻底搞清楚的开发同学。1. 内容整体设计与思路拆解1.1 锁问题的本质并发场景下的资源争夺在正式进入排查操作之前咱们先把底层的逻辑捋清楚。GBase 8c 里的锁机制说白了就是一套“资源的预约系统”当一个事务想要读取或者修改某一行数据时它必须先获得对应的锁。如果这行数据正被别的事务占着那这个事务就只能排队等。听上去很简单但实际生产环境里事务并发数量动辄几百上千锁的类型又分成行级锁、表级锁、咨询锁等多种排列组合之后锁等待的场景就变得非常复杂。我遇到过一个很典型的案例业务侧同时有批量导入任务和在线交易任务在跑导入任务先锁住了一张大表的整表元数据在线交易任务想往这张表里插入数据卡在了表锁等待上结果交易超时率一路飙到 30%。事后查看才发现导入事务因为一个外部接口调用迟迟没有提交把整张表的写路径全堵死了。这就是锁冲突最常见的成因——不是数据库本身出了问题而是事务生命周期管理没做好。1.2 设计排查方案时的核心考量面对锁问题我个人的排查思路从来都是“查源头不堵结果”。很多初学者一看到有锁等待就直接 kill 会话这其实是在治疗症状而不是治疗病根。你kill了一个会话过一会儿新的业务请求又来了同样的锁冲突照样发生因为产生锁冲突的代码逻辑没变。所以设计一套完整的排查方案时我会按照“查看当前锁状态 → 定位等待关系 → 分析阻塞源头 → 针对性处理 → 优化长期方案”这个链路来走。接下来要讲的每一条命令、每一个视图都是围绕这条链路展开的。这套方法我自己在 GBase 8c 上反复验证过多次也适配绝大多数基于 PostgreSQL 内核的数据库产品换到其他数据库上思路同样成立。2. 核心细节解析与实操要点2.1 GBase 8c 锁的两大类型表锁与行锁先说表锁这是最容易引起大面积业务阻塞的锁类型。GBase 8c 的表锁包括 ACCESS SHARE访问共享锁、ROW SHARE行共享锁、ROW EXCLUSIVE行排他锁、SHARE UPDATE EXCLUSIVE共享更新排他锁、SHARE共享锁、SHARE ROW EXCLUSIVE共享行排他锁、ACCESS EXCLUSIVE访问排他锁等好几种级别。它们的兼容关系各不相同比如 ACCESS EXCLUSIVE 和几乎所有其他锁都互斥一旦某个会话持有这张表的 ACCESS EXCLUSIVE 锁其他会话连 SELECT 都会被堵住。行锁则精准得多它是 MVCC 机制的基石。在 GBase 8c 里一个事务对某一行执行 UPDATE 或 DELETE 时会在这行数据上留下一个行级排他锁其他事务要修改同一行就只能等待。行锁的好处是并发度高不同行的更新互相不影响坏处是一旦发生跨行的循环更新死锁的风险就出现了。这里给你一个实用的参考表方便快速判断锁冲突的严重程度锁类型典型触发命令阻塞范围冲突对象ACCESS EXCLUSIVEDROP TABLE、TRUNCATE、VACUUM FULL所有读写操作所有类型的锁SHARE创建索引非并发模式写操作被阻塞读操作可继续ROW EXCLUSIVE 及以上ROW EXCLUSIVEINSERT、UPDATE、DELETE不阻塞 SELECT阻塞其他写操作SHARE 及以上ACCESS SHARESELECT仅与 ACCESS EXCLUSIVE 冲突ACCESS EXCLUSIVE2.2 死锁产生的四个必要条件与 GBase 8c 的处理机制说到死锁教科书上总结的四个必要条件这套理论在 GBase 8c 这里依然适用互斥条件、持有并等待、不可剥夺、循环等待。一句话概括就是事务 A 拿着资源 1 在等资源 2事务 B 拿着资源 2 在等资源 1两边谁都不松手就形成了死锁。GBase 8c 默认开启了死锁检测机制具体由参数 deadlock_timeout 控制默认值是 1 秒。这个参数的意思是当一个会话等待某个锁超过 1 秒时后台的死锁检测线程就会被唤醒去检查整个等待图里是否存在环路。如果确实检测到了死锁数据库会主动牺牲其中一个事务通常是被检测到成本较低的那个然后向客户端抛出类似deadlock detected的错误信息。但是注意了死锁检测只能发现已经形成的死锁没办法预测死锁。而且如果业务里锁等待的环路不是刚好 1 秒形成而是长时间累积才出现的deadlock_timeout 设置得太小反而容易误判普通锁等待为死锁。我一般建议生产环境设置在 1 到 2 秒之间太激进反而会引入不必要的开销。2.3 视图查询定位锁状态的关键入口GBase 8c 提供了几个非常重要的系统视图用于查看锁信息排查时主要用到的有三个。第一个是 pg_locks它记录了当前数据库集群中所有的锁信息。每一条记录都包含被锁对象、持有锁的会话 ID、锁类型、锁状态granted 表示已获得waiting 表示正在等待等字段。这是查锁冲突最直接的入口。第二个是 pg_stat_activity它记录了每个会话的当前状态包括会话 ID、用户、数据库、客户端地址、正在执行的 SQL、事务开始时间等关键信息。排查时必须把 pg_locks 和 pg_stat_activity 关联起来看才能把“锁被谁占着”和“这个会话在干什么”对应上。第三个是 pg_blocking_pids严格来说它是一个函数输入一个会话的 PID返回阻塞它的会话 ID 列表。这个函数在追查阻塞源头时简直太好用了一条 SQL 就能把锁等待关系串起来。下面这条 SQL 是我每次排查锁问题时必跑的“黄金查询”用来列出现场所有锁等待和被阻塞的会话信息SELECT a.pid AS blocked_pid, a.usename AS blocked_user, a.query AS blocked_query, b.pid AS blocking_pid, b.usename AS blocking_user, b.query AS blocking_query FROM pg_stat_activity a JOIN pg_stat_activity b ON a.pid ANY(pg_blocking_pids(a.pid)) ORDER BY a.pid;执行之后你会一目了然地看到谁在等锁、谁在持锁、两边各自在跑什么 SQL。基于这个结果再往下分析会比对着 pg_locks 手工拼接高效得多。3. 实操过程与核心环节实现3.1 一次真实的行锁冲突排查全过程下面我分享一个今年上半年处理过的真实案例完整的排查链路具备很好的参考价值。现象是什么呢业务方反馈某个核心交易接口响应时间从原来的 50 毫秒暴涨到 8 秒数据库 CPU 占用率并不高但应用连接池里的连接几乎被占满。一看这个现象我第一反应就是锁等待而不是性能瓶颈——CPU 不高、连接占满典型的“会话堵在锁上不干活”的特征。我先执行了上面那条黄金查询结果出来了三行数据也就是说有三个会话被阻塞。每个被阻塞的会话都在执行同一条 UPDATE 语句更新的是同一个客户 ID 的账户余额。再看阻塞源头的会话PID 是 28371执行的事务已经开启了 40 多分钟但它当前的 SQL 显示为空状态是 idle in transaction。这就有意思了。阻塞者的 SQL 是空的说明它的事务已经执行完最后一条语句但事务一直没有提交或回滚所有它占用过的行锁都没有释放。这个会话到底卡在哪儿了我查了它的等待事件发现它正在等待一个外部 HTTP 接口的响应——应该是应用代码里在事务中间调用了外部服务结果外部服务超时事务就一直挂着不提交。这种情况下数据库本身一点错都没有纯粹是应用层把事务生命周期拖得太长了。3.2 处理策略该等还是该杀面对这种锁堵塞处理策略要分情况看。如果阻塞时间还短外部接口很快会恢复那等一等是值得的如果像这个案例一样已经堵了 40 多分钟还没有任何要结束的迹象那就不用客气了直接终止阻塞源头的会话。-- 终止阻塞源头的会话谨慎操作 SELECT pg_terminate_backend(28371);执行完这条命令阻塞会话被强制终止事务回滚所有行锁释放。紧接着三个等待中的 UPDATE 事务依次获得锁并完成执行业务接口响应时间恢复到 45 毫秒左右。但我得特别提醒一点 kill 之前一定要和业务方确认或者至少检查一下这个会话里有没有未提交的重要操作。pg_terminate_backend 会把这个会话中未提交的事务全部回滚如果里面有重要的业务数据修改可能造成数据丢失。生产环境里没有百分之百把握不要轻易执行 kill能先和业务沟通就先沟通。3.3 从锁等待视图深挖找出等待链条的全貌刚才的案例只涉及一层阻塞但复杂的生产场景里经常出现“链式等待”——A 等 BB 等 CC 等 D甚至形成多个环节的链条。这种情况下单纯靠黄金查询一条一条地看非常低效。我习惯用一条递归查询把整个等待链条完整拉出来。GBase 8c 支持 WITH RECURSIVE 语法可以很方便地实现这个目标WITH RECURSIVE lock_chain AS ( -- 找出所有被阻塞的会话作为起点 SELECT a.pid AS waiter, b.pid AS blocker, 1 AS depth, a.query AS waiter_query, b.query AS blocker_query FROM pg_stat_activity a JOIN pg_stat_activity b ON a.pid ANY(pg_blocking_pids(a.pid)) WHERE a.state idle UNION ALL -- 递归找上一级的阻塞者 SELECT c.pid, d.pid, lc.depth 1, c.query, d.query FROM lock_chain lc JOIN pg_stat_activity c ON c.pid lc.blocker JOIN pg_stat_activity d ON c.pid ANY(pg_blocking_pids(c.pid)) WHERE lc.depth 10 ) SELECT * FROM lock_chain ORDER BY depth DESC;这条 SQL 的输出能把阻塞链条的完整层次展示出来最底层的 depth 最大的那一行就是整个等待链条的根因会话。实际用下来这种先建立全局“堵塞地图”的做法比逐个会话去查效率高出不止一个量级。3.4 定位问题 SQL从 pg_stat_activity 到执行计划在解决锁问题的基础上还需要搞清楚一个更深层的问题为什么这些 SQL 会产生锁冲突我遇到过很多次同样的业务逻辑有的环境跑得很顺畅有的环境频繁死锁差异就在于 SQL 的执行计划走的路径完全不同。比如更新某一行数据时如果 WHERE 条件能命中主键索引数据库就只锁那一行但如果 WHERE 条件无法走索引数据库需要先在表里扫描定位目标行扫描过程中会对扫描路径上的一些行甚至整张表加锁锁的范围就大了很多。这就是我前面说的“实际影响范围比理论预想大得多”的典型场景。所以遇到频繁锁冲突的 SQL一定要用 EXPLAIN 看执行计划确认是不是走了索引EXPLAIN (ANALYZE, BUFFERS) UPDATE account_balance SET balance balance - 100 WHERE account_id A10086;如果执行计划里出现了 Seq Scan全表扫描而 account_id 明明有索引很可能是统计信息过期导致优化器选错了计划。这种情况下先执行 ANALYZE 更新统计信息往往比调整 SQL 本身更有效。3.5 集群版与单机版的排查差异GBase 8c 有两种部署形态单机版和分布式集群版排查锁冲突时有一些差异需要注意。单机版相对简单所有锁信息都在同一个实例上查看 pg_locks 就能看到全貌。分布式集群版则复杂一些数据分布在多个节点上一个事务可能涉及多个节点的锁操作。在集群版上排查锁冲突时需要先确认阻塞发生在哪个节点因为不同节点上的 pg_stat_activity 是独立的。有一个实用的小技巧在集群版中可以通过查询 pgxc_node 视图确认节点列表然后逐个节点执行锁查询也可以用 GBase 8c 提供的全局视图进行跨节点查看。如果你的环境里配置了全局会话管理工具优先使用它会比手工逐个节点登录省力很多。4. 常见问题与排查技巧实录4.1 死锁检测机制不生效怎么办有个读者问我数据库明明发生了死锁为什么等了很久都没有抛出“deadlock detected”错误我看了一下他的环境配置发现 deadlock_timeout 被设置成了 5 秒。这本身不是问题问题在于他把整个系统调得过于保守同时大量长事务同时在跑死锁检测需要遍历的等待图节点非常多检测开销变大发现死锁的时间会明显变长。排查建议分两步走。第一步先确认参数生效没有SHOW deadlock_timeout;如果返回值是 1s 或 2s 左右说明机制正常那就继续追踪 pg_locks 里是否有两个会话互相等待的状态。第二步如果检测确实没有触发手动分析当前锁等待图中是否存在环路。最简单的方法是把黄金查询的结果画成一张等待关系表看能否构成 A→B→A 的闭环。如果有闭环而数据库没报错那大概率是检测线程本身出了问题需要检查数据库日志中是否有死锁检测相关的报错。4.2 锁等待一直不消失杀会话也没用这种情况我碰到过才意识到问题可能不在数据库层。有一次排查锁冲突我 kill 了阻塞源头但锁依然没有释放新查询依然卡住。仔细检查才发现应用层的连接池会自动重建被 kill 的会话并且立即重放未完成的事务。相当于我这边刚 kill 完那边马上又开了一个新事务做同样的事情锁当然不会释放。解决思路是把应用层的重试机制先停掉或者把连接池的自动提交行为临时关掉从源头掐断“无限重试”的循环。另外有些应用框架如果配置了事务超时自动重连也会放大锁问题。排查锁冲突时不要只盯着数据库的视图和命令也要留意应用侧的逻辑。4.3 GBase 8c 中常见的死锁场景与规避方法基于我自己的实战经验GBase 8c 生产环境中最常见的死锁场景有这几类每一类都有对应的规避手段。第一类是批量更新同一张表的多行数据时多条并发事务以不同的顺序更新相同的行集合。比如事务 A 先更新 id1 再更新 id2事务 B 先更新 id2 再更新 id1如果时间节奏恰好踩上就形成了循环等待。规避方法是在业务代码里固定更新顺序统一按照主键排序后再执行更新或者使用 SELECT ... FOR UPDATE 先把需要的行一次性锁定。第二类是主从表结构中的外键约束引发的问题。更新父表时数据库需要检查子表中是否有引用这个过程可能给子表加锁而另一边事务正在更新子表导致两边互相等待。规避方法是在事务开始前按照固定顺序加锁父表和子表避免交叉等待。第三类是应用在事务中调用外部服务导致事务长时间挂起期间涉及的所有行都被锁住其他事务只能排队等待。这个虽然没有形成严格意义的死锁但会引发大规模的锁等待效果和死锁几乎一样。规避方法非常明确绝不在数据库事务中调用外部接口或者执行网络耗时操作。4.4 快速定位瓶颈会话的 SQL 速查表每次排查锁问题都要敲一堆 SQL我觉得不如整理一个速查表放在手边遇到问题直接复制粘贴改一改效率能提高不少。排查需求具体操作查看所有锁信息SELECT * FROM pg_locks;查看活动会话SELECT pid, usename, state, query, xact_start FROM pg_stat_activity WHERE state idle;找出阻塞者SELECT pid, usename, query FROM pg_stat_activity WHERE pid IN (SELECT unnest(pg_blocking_pids(指定PID)));查看锁等待关系SELECT blocked.pid, blocker.pid FROM pg_stat_activity blocked JOIN pg_stat_activity blocker ON blocked.pid ANY(pg_blocking_pids(blocker.pid));终止指定会话SELECT pg_terminate_backend(会话PID);取消当前查询但不结束事务SELECT pg_cancel_backend(会话PID);查看死锁相关参数SHOW deadlock_timeout;更新统计信息ANALYZE 表名;补充一个小知识点pg_terminate_backend 是强制终止整个会话事务会回滚pg_cancel_backend 只是取消当前正在执行的 SQL如果事务里还有其他语句事务本身不会回滚。优先考虑用 pg_cancel_backend实在不行再用 pg_terminate_backend这是个好习惯。4.5 一个典型的死锁日志解读最后用一个真实的死锁日志片段来演示怎么解读。GBase 8c 的死锁日志会记录在数据库服务日志文件中内容类似ERROR: deadlock detected DETAIL: Process 28371 waits for ShareLock on transaction 189032; blocked by process 28402. Process 28402 waits for ShareLock on transaction 189031; blocked by process 28371.这里 ShareLock 是行锁的一种表现形式transaction 189032 和 189031 分别是两个事务的 ID。日志说得很清楚会话 28371 在等事务 189032 上的锁而这个事务被会话 28402 占着同时会话 28402 在等事务 189031 上的锁而这个事务被会话 28371 占着。这就是教科书级别的循环等待。打开日志的目的不是看个热闹而是确认两个会话正在执行的 SQL 语句。日志里通常还会包含 STATEMENT 字段记录每个会话最后执行的 SQL。把两条 SQL 对照业务逻辑看就能还原出死锁的真实触发场景之后才能做针对性的代码修复。5. 预防策略与长期优化建议5.1 事务设计层面的四个硬性要求死锁一旦发生谈再多“排查技巧”都只是亡羊补牢。真正的高手会把精力花在预防上。根据死锁的四个必要条件我总结了四条在 GBase 8c 上的工程化预防策略。第一保持事务短小。把大事务拆成若干小事务每条事务只处理必要的数据。事务持续时间越短持有锁的时间越短和其他事务发生交集的可能性就越低。我在压测环境里做过对比同样一批数据更新拆成小事务之后锁冲突率下降了将近 70%。第二固定访问顺序。多条事务如果需要更新多行数据尽量按照同样的顺序进行。比如业务中经常同时更新订单表和商品库存表那就约定所有代码都先更新订单表再更新库存表这样就不会出现互相持有对方需要的锁的情况。第三避免在事务中做耗时操作。这一点前面反复强调过但值得再提醒一次。事务中的每一个外部调用、每一次大表全表扫描都是在拉长锁的持有时间。如果业务确实需要在事务中查询外部数据先把数据查好再开启事务。第四合理使用索引。确保 UPDATE 和 DELETE 语句的 WHERE 条件能够走索引避免全表扫描带来的大范围锁。定期 ANALYZE 维护统计信息帮助优化器做出正确的执行计划。5.2 监控预警主动发现而不是被动救火排查锁问题和很多运维工作一样最佳的时机是在它造成业务影响之前。如果你的环境里还没有针对锁等待的监控我建议你尽早搭一套简单的预警机制。最简单的方式是写一个定时脚本每 30 秒执行一次黄金查询如果发现有会话等待锁超过 5 秒就把相关信息推送到告警群。这里明确一点这个阈值要结合业务的正常水平来定。有的核心交易系统要求锁等待不超过 1 秒有的批量业务场景锁等待 30 秒都算正常。阈值定得太低会被告警噪音淹没定得太高又起不到预警作用。我个人习惯是先观察一周的正常水位然后取正常水位的 3 到 5 倍作为告警阈值。5.3 上线前的锁风险评估最后分享一个很多人会忽略的环节——新功能上线前的锁风险评估。我见过太多死锁事故都是新代码上线之后立刻爆发的。原因是很多开发同学写 SQL 时只关注了功能正确性完全没有考虑并发场景下的锁行为。所以在 GBase 8c 上线新功能之前我会要求开发团队回答几个问题这个功能涉及哪些表的哪些操作预计并发量是多少事务里有没有外部调用多条事务更新同一批数据时顺序是否一致如果这些问题都回答不清楚那就先在测试环境里用高并发压测工具模拟一下真实场景再上线。这套流程在过去帮我挡下了至少三次可能引发线上死锁事故的发布。6. 写在最后说实话锁冲突和死锁排查这件事表面上看是技术问题实际上考验的是对整个系统的理解深度。你不仅要懂数据库的锁机制、MVCC 原理、系统视图还要能看懂应用代码的事务边界、连接池策略甚至外部服务的超时配置。任何一个环节有盲区都可能在关键时刻掉链子。回到 GBase 8c 本身它的锁机制基于成熟的开源内核演进而来稳定性是经过大量生产环境验证的。绝大多数锁问题都不是数据库“不行”而是使用方式出了问题。把事务设计规范起来把监控预警完善起来把排查手段练熟练透你就能从“被锁问题追着跑”变成“提前发现并解决”。我在实际运维中养成了一个习惯每次排查完一个锁问题都会把完整的处理过程记录下来包括当时的现象、用到的命令、分析思路、最终解法。时间长了这本地文档成了最有价值的参考资料。遇到类似问题时直接翻出来对照不用从零开始分析。也建议你这么做踩过的坑记得越详细以后绕开它的速度就越快。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。