5.2 锁机制、阻塞链与死锁复盘


5.2 锁机制、阻塞链与死锁复盘

本节摘要:锁把隔离级别的规格落实到每一行、每一页;阻塞是锁的排队,死锁是排队的环。本节先讲清锁模式与资源层级、锁升级的触发条件,然后给出阻塞链现场还原的标准查询,最后完整尸检一桩双事务死锁——从拿到死锁图到输出三类整改方案的全过程。这是本章最贴近值班实战的一节。

一次死锁告警的完整尸检

凌晨两点收到死锁告警:订单服务与库存服务互踩,库存事务被选为牺牲者整体回滚。尸检第一步是拿证据——扩展事件里的死锁图(XML),里面记着两个会话各自的进程号、持有的锁、等待的锁、执行的语句。还原交错过程:会话 A(订单服务)先更新订单表某行,再更新库存表同一行;会话 B(库存服务)先更新库存表同一行,再更新订单表某行。两条路径一交叉,A 攥着订单行等库存行,B 攥着库存行等订单行,环成,检测线程五秒内选中代价小的 B 回滚放行。死锁不是 bug,是"加锁顺序不一致"这个设计缺陷的必然产物——优化器只负责发现并打破它。

整改按见效速度排三招。速效招:统一加锁顺序——两个服务的事务都按"先库存后订单"(或全局统一按主键排序更新),环无法形成,这是治本。索引招:尸检里库存服务的 UPDATE 走了全表扫描,扫过的每一行都挂了更新锁再释放,无意间把加锁范围扩大到几千行;给更新谓词补上索引,让锁精准落到目标行,冲突窗口立刻收窄——很多"死锁频发"的根因其实是缺索引导致的扫描放大,与第 4 章的知识在这里会师。降档招:库存校验逻辑若可容忍,把事务里的读改成快照读,缩短持锁时间。三招全上后,该系统死锁从日均二十次降到月均一两次。

锁模式与资源层级:秩序的坐标系

锁模式是一张互斥关系表:共享锁(读)之间兼容;排他锁(写)与一切互斥;更新锁是"读到想改"的中间态,彼此不兼容、与共享锁兼容,用于更新前的扫描阶段,避免两个会话同时升级为排他锁的争抢;意向锁不锁数据本身,只在层级节点上挂"下面有人"的路标,让表级的快速判断成为可能;架构锁在 DDL 时护住结构定义。资源层级从大到小是数据库、表、页、行,申请行级锁前会先在页与表挂意向锁,构成层级一致的加锁网络。

锁升级是这套体系里的"省内存机制":单语句累计约五千把锁,或锁管理器内存吃紧时,细粒度锁收拢成一把表级锁。好处是锁结构内存骤降,坏处是全表排队。触发场景高度模式化:大批量 UPDATE 与 DELETE、缺索引导致的宽扫描。预防两板斧:大批量操作拆批(每批几千行,批间提交);为操作谓词建索引把锁范围钉死。表级锁提示(TABLOCKX 类)是外科手术工具,只在批量装载等明确需要独占的窗口使用,绝不留在常驻代码里。

-- 阻塞链现场还原:谁在等、等谁、等多久、哪条语句 SELECT wt.blocking_session_id AS 堵路者, wt.session_id AS 受害者, wt.wait_type AS 等待类型, wt.wait_time AS 已等毫秒, wt.resource_description AS 被占资源, t.text AS 受害者语句 FROM sys.dm_os_waiting_tasks wt CROSS APPLY sys.dm_exec_sql_text( (SELECT sql_handle FROM sys.dm_exec_requests WHERE session_id = wt.session_id)) t WHERE wt.blocking_session_id <> 0 ORDER BY wt.wait_time DESC; -- 从受害者顺着堵路者字段向上爬到链头;链头会话若在正常运行且业务允许, -- 优先与业务确认而非直接 KILL——KILL 只解决这次,解决不了结构。

阻塞治理:区分"该杀"与"该等"

阻塞不全是病。一个正在跑的合法大事务会短暂阻塞别人,盲目 KILL 反而制造更长的事务回滚。值班判定看三个数:等待类型是 LCK 开头才归锁治理管;等待时长是否越过业务阈值(下单接口超过三秒即事故);链头会话的语句是否长期无进展(open 的交易挂了几十分钟多半是应用异常没提交,这类必须处理并追应用连接泄漏)。结构治理三板斧依旧是:缩短事务(事务里不做外部调用与人工交互)、收窄锁范围(索引精准命中)、读走快照(报表与交易解耦)。把这十四个字贴在运维手册第一页,能省掉一半的深夜电话。

锁诊断的进阶工具箱

阻塞链查询之外,三个进阶工具补全诊断纵深。锁清单视图(sys.dm_tran_locks):按资源与请求者列出当前所有锁与等待,阻塞链告诉你谁堵谁,锁清单告诉你堵的资源到底是谁持有的哪把锁——两者交叉才能定位到"某存储过程持有的页锁"。阻塞过程报告:默认引擎不记录超过阈值的阻塞,把阻塞阈值设为五秒并配置扩展事件的阻塞报告事件,每次长阻塞都会自动留档(阻塞链快照加语句),事后复盘不再依赖"当时有人在线上查"。锁升级事件:把锁升级事件纳入扩展事件常驻采集,批量操作什么时候、在哪个对象上触发了表锁,一目了然——它把"怀疑锁升级"变成"看到锁升级"。
死锁侧还有一个常被忽略的预防参数:死锁检测间隔。默认五秒的检测周期在极端高并发下会放大牺牲者数量,但调它属于最后的手段——绝大多数死锁频发,根因仍是加锁顺序与索引覆盖,先把 5.2 的三招整改做扎实,再考虑动引擎参数。

本节要点回顾

  • 死锁是设计缺陷的产物:加锁顺序不一致成环,检测线程打破环并回滚代价小的一方;
  • 尸检三要素:死锁图里的语句、持有的锁、等待的锁,完整复述交错过程再谈整改;
  • 三类整改:统一加锁顺序治本、补索引收窄锁范围、快照读缩短持锁时间;
  • 锁模式分工:共享兼容、排他独裁、更新锁防争抢、意向锁搭路标,层级从库到行;
  • 锁升级约五千把触发:大批量操作拆批加精准索引是标准预防,表提示只在明确窗口用;
  • 阻塞治理十四字:缩短事务、收窄锁范围、读走快照——先判该杀该等,再动手。

悲观并发的秩序讲透了。但如果干脆不排队、先干活再验冲突呢?下一节走进内存优化表的世界——乐观并发的另一种活法。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U