深夜的环形停车场里,四辆货车 T1、T2、T3、T4 各自占住一个卸货位,又都想再顺路停进下一个车位:T1 占了 1 号位想进 2 号位,T2 占了 2 号位想进 3 号位……四辆车首尾相咬,谁都不肯退让,整个停车场彻底堵死。这就是数据库世界里最常见的并发事故——死锁(Deadlock)。停车场的值班管理员有两件事要做:第一,及时发现“四辆车堵成环”的事实(死锁检测);第二,从四辆车里选一辆损失最小的请它倒车出去,让其他车先走(死锁处理)。本文就用这座环形停车场的故事,为你拆解死锁检测与处理的完整思路:从等待图检测环路,到超时兜底,再到受害者选择与不同数据库的实践差异。
掌握死锁的检测原理与处理策略,是数据库性能调优与高并发架构设计的基本功,也是面试和实战中避开“数据库卡死”事故的关键能力。
可视化演示
展示等待图环路检测死锁、超时检测机制触发回滚及牺牲者选择策略,交互切换理解数据库死锁检测与处理核心流程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
等待图WFG环路检测 事务等待关系构建有向图 图中存在环即判定死锁 WFG Cycle 等待图是死锁检测的核心数据结构
死锁成因剖析
深夜环形停车场,T1 占了 1 号位、还想进 2 号位;T2 占了 2 号位、还想要 3 号位;T3 占了 3 号位、盯上 4 号位;T4 占了 4 号位、偏偏要回 1 号位。四辆车首尾相接,每辆车的路都被下一辆堵住。停车场管理员跑过去一看就明白了:这不是谁技术差,而是大家互相攥着对方要的东西不放——这就是死锁的雏形。
数据库里,事务就是司机,行锁就是车位。事务 A 更新了第 1 行(拿着 1 号位),还想更新第 2 行;事务 B 更新了第 2 行还想更新第 3 行……一旦形成一个“你等我、我等他”的圆圈,谁都无法前进。专业定义是:两个及以上事务在并发执行中,各自持有一个资源,又都在等待对方持有的资源,导致所有事务永久阻塞,称为死锁。
死锁的发生有四个必要条件:互斥、持有并等待、不可抢占、循环等待。四者同时满足才会死锁,因此所有检测与处理策略的本质,都是围绕打破其中之一做文章。比如“主动避免策略”就是打破循环等待。公式如下:
所以管理者心里有本账:只要让四辆车别“无限期攥着车位不放”,或者别让它们形成圆圈,就能从根上防住死锁。后文讲的检测、超时、回滚、排序,都是从这个思路长出来的。
- 互斥条件车位一次只能停一辆货车,行锁一次只能被一个事务持有,这是资源本身的性质。
- 持有并等待条件货车占着已有车位还等下一个车位,事务拿着已有锁还申请新锁——这是形成僵局的前提。
- 不可抢占条件占着的车位不能被别人强行挪走,数据库里的锁也不能被其他事务强行夺走,只能等持有者主动释放。
- 循环等待条件四辆车像串珠一样互相等待,形成闭环;没有环就没有死锁。
- 蕴含关系箭头表示“一旦死锁成立,左边成立则右边必然同时成立”,四个条件缺一不可。
等待图检测法
与急脾气的管理员不同,MySQL InnoDB 的管理员很精细——他在监控室挂一块白板,每当一辆车说“我等 X 车位”,就画一条从这辆车指向车位持有者的箭头。这块白板,就是等待图。箭头越多,白板上的关系越复杂,但只要箭头构成一个圈,管理员立刻就能判断:出事了。
数据库的等待图是有向图 $G_t=(V,E)$,其中 $V$ 是所有活跃事务,$E$ 是等待关系:$T_i \to T_j$ 表示 $T_i$ 在等 $T_j$ 持有的锁。事务申请锁时加一条边,获取锁或回滚时删一条边。只要途中出现有向环,就必然存在死锁。InnoDB 的做法是:每次产生新的等待边时,增量检查一次图中有没有环,属于“实时检测”。
环检测用深度优先搜索(DFS):从任意顶点出发递归遍历,如果当前顶点的某个邻居已经在“本次递归路径栈”里,说明绕了一圈又回到自己,即找到环。等待图的数学定义与判定条件如下:
管理员盯着白板,箭头一旦绕成一圈,他就立刻抓起喇叭喊:“死锁!” 这种检测方式不依赖猜测,只要图里有环就一抓一个准,这也是主流数据库的核心检测思路。
- 顶点集合 V图中每个节点是一个活跃事务,相当于白板上每辆车的名字。
- 边集合 E每条有向边是一次“等待”记录,从等待者出发指向被等待者。
- 有向边含义$(T_i, T_j)$ 读作“$T_i$ 在等 $T_j$”,箭头方向不能画反。
- DFS 回溯判定深度优先遍历时,若遇到一个已经在递归栈中的顶点,说明路径绕成环,死锁成立。
等待超时策略
有些简单的停车场,管理员不看白板,只发计时小票:每辆车进入等待状态就掐表,等待超过一定秒数,不管是不是真死锁,直接请出场。这就是等待超时策略。它的逻辑非常直白——僵持太久的一定有问题,先赶走再说。
数据库里,innodb_lock_wait_timeout 就是锁等待超时参数,默认 50 秒。一个事务等待某个行锁超过该时长,数据库就让这个事务报错并回滚。优点是实现简单,不依赖任何图结构;缺点也很明显:正常的长时间事务(比如跑大报表、批量更新几百万行)可能只是排队等锁等得久,却被误判成死锁杀掉,这就是“误杀长事务”。
超时判定的公式可以写成:
急脾气管理员虽然果断,但容易误伤老实车。所以现代数据库通常把等待超时当成“兜底保险”:先用等待图精准检测,万一检测机制出问题或事务卡住,再由超时机制断后。
- 等待时间 $t_{\text{wait}}$事务从开始等某一个锁到当前时刻的累计时长,相当于小票上的秒数。
- 阈值 $\tau$管理员设定的等待上限,在 MySQL 中就是
innodb_lock_wait_timeout的值,默认 50 秒。 - 比较符号 $\geqslant$等待时间一旦达到或超过阈值,条件即触发。
- 回滚动作触发后数据库直接回滚 $T_i$,释放它持有的所有锁,让其他事务继续。
受害者选择
管理员确认死锁后,必须请一辆车退出去。请谁呢?T1 已经卸了半车货,T4 才刚停进去——当然是请 T4 退,损失最小。这个“损失最小”就是受害者选择的直觉:不是惩罚谁,而是让整个停车场付出的代价最低。
数据库里,InnoDB 实际会选择“undo log 行数最少”的事务回滚,近似理解为“已完成工作最少、回滚最便宜”的事务。更通用的代价函数可以综合三个因素:事务已运行时长 $A$、已完成的更新行数 $W$、已持有的锁数量 $R$,再给它们配上不同的权重系数。计算公式如下:
选定受害者之后,还要决定回滚到什么程度。默认是“全车倒出”——整个事务回滚,所有修改作废;有些数据库支持保存点(Savepoint),可以只回滚到保存点,保留之前的工作,这就是“回滚粒度”的差别。管理员专挑损失最小的车请走,整体代价最低,这就是死锁处理的核心智慧。
- 运行时长 $A(T)$事务已经执行了多久,时间越长说明它可能已经做了很多事,回滚越可惜。
- 已完成工作量 $W(T)$事务已经更新了多少行数据,行数越多,回滚需要撤销的修改越多。
- 持锁数量 $R(T)$事务当前握着多少把锁,锁越多,回滚时要释放的资源越多,影响面越大。
- 权重系数 $\alpha, \beta, \gamma$对不同因素的重视程度,比如把 $W$ 的权重调高,就更倾向于回滚“干活少”的事务。
- 总代价 $C(T)$加权求和后得到该事务的回滚代价,数据库挑选 $C$ 最小者作为受害者。
主动避免策略
如果管理员更负责,他会规定所有车进入停车场后必须按“车位号从小到大”的顺序停靠:先找 1 号位、再去 2 号位、最后 3 号位。这样一来,四辆车都朝着同一个方向行驶,永远不可能绕成环。与其等死锁发生后再手忙脚乱地处理,不如从规则上让它根本发生不了。
数据库里的对应做法是“资源统一排序”(Lock Ordering):所有事务严格按照同一全局顺序获取资源。比如在代码里约定,操作多条记录时一律按主键 ID 升序加锁,先锁 id=1 再锁 id=2,而不允许某个事务先锁 id=2 再锁 id=1。这个规则直接打破了“循环等待”这一必要条件。公式化表达为:
除了资源排序,还有一种“一次性获取”的思路:把多条记录的加锁合并到一条 SELECT ... FOR UPDATE 或 UPDATE 语句中,让数据库在一瞬间同时拿到所有需要的锁,而不是逐个申请。再配合锁超时兜底,即使有事务违背约定,也不会无限期卡死。规矩定好了,环自然断。
- 资源全序 $\prec$给所有资源(车位、行锁)排一个全局顺序,定下“谁在前谁在后”。
- 传递链条资源按 $R_1, R_2, \ldots, R_n$ 排成一条链,不允许多叉。
- 事务约束任何事务获取多个资源时,只能按这个顺序从前往后拿,不能跳序、不能逆序。
- 反证无环假设存在循环等待 $R_i \succ R_j \succ \cdots \succ R_i$,则意味着顺序上 $R_i$ 既小于又大于自己,矛盾,所以环不可能出现。
数据库实现对比
不同停车场的规矩各不相同。MySQL InnoDB 的管理员眼疾手快,白板上一出现环就立刻处理;PostgreSQL 的管理员按固定节奏巡场,每隔一小段时间检查一次,发现死锁就向司机报错;SQL Server 管理员则每几秒做一次全面扫描,还会给每辆车标一个“牺牲优先级”,决定先请谁出场。
技术层面看:MySQL 的 InnoDB 在等待边发生变化时即时增量检测,回滚 undo 最少的事务,可用 innodb_deadlock_detect 参数开关检测功能;PostgreSQL 的锁管理器周期性检查进程间的等待关系,检测到死锁后终止其中一个事务,并向客户端返回 "deadlock detected" 错误;SQL Server 的死锁监视器周期扫描锁等待,支持用 SET DEADLOCK_PRIORITY 指定事务的牺牲优先级,并将死锁图写入错误日志供分析。
检测频率越高,发现越及时,但 CPU 开销也越大。单次环检测的复杂度是 $O(V+E)$,$V$ 是等待图的顶点数,$E$ 是边数;假设每秒检测 $f$ 次,总开销就是两者的乘积:
选哪种方案完全看业务:高并发电商系统更看重快速解除僵局,愿意为实时检测付出 CPU 成本;离线分析系统死锁极少,用低频率周期检测就足够省心。
- 单次检测复杂度 $O(V+E)$对等待图做一次 DFS 环检测,需要遍历所有顶点和边,计算量与它们之和成正比。
- 检测频率 $f$每秒钟做几次检测,等于检测间隔 $\Delta t$ 的倒数。
- 总开销公式频率越高,每秒被重复执行的图遍历次数越多,累计 CPU 开销线性增长。
- 权衡启示追求实时性就调小 $\Delta t$,追求低开销就调大 $\Delta t$,没有免费午餐。
性能调优实践
管理员最终的考核标准是运营报告:停车场每晚堵死几次?每次回滚了几辆车?他需要一套指标和工具来持续监控。数据库运维也是同样道理,光会处理死锁不够,还要会量化、会调优。
常见的指标是死锁率(Deadlock Rate),定义为一个时间段内死锁次数与总事务数的比值。优化手段包括:一是缩短事务——事务越短,持锁时间越短,死锁概率越低;二是统一加锁顺序,避免应用层代码各行其是;三是减小锁范围,让 SQL 走索引、少扫行;四是在 MySQL 中调整 innodb_deadlock_detect 和 innodb_lock_wait_timeout 参数;五是查看 SHOW ENGINE INNODB STATUS 中的最近死锁日志,分析等待环后调整 SQL。死锁率公式如下:
管理者心中有指标、手里有工具,停车场就不会再彻夜堵车。从“出事了再抓环”到“让环根本形成不了”,死锁检测与处理这门功夫,最终都落在“让数据库在高并发下依然稳定可用”这一个目标上。
- 死锁次数 $N_{\text{deadlock}}$从错误日志或状态变量中统计出的死锁发生次数。
- 提交事务数 $N_{\text{commit}}$统计周期内成功提交的事务总数,代表正常完成的工作量。
- 回滚事务数 $N_{\text{rollback}}$统计周期内回滚的事务总数,包含死锁回滚与业务主动回滚。
- 死锁率 $DLR$两者相除得到死锁占比,是衡量并发健康度的重要指标,调优目标就是让它尽量趋近于零。
AI 应用场景
电商秒杀扣库存
阶段:应用层事务设计 + 数据库死锁恢复
解决的问题:秒杀场景下,大量订单并发扣减同一批热门商品的库存。传统写法是“先扣库存、再插订单、再更新商品热度”,多个事务获取锁的顺序不一致,很容易形成环形等待,导致订单接口大面积超时。数据库的死锁检测会自动回滚其中一个事务,应用层捕获死锁异常后重试有限次数,让其他请求继续成功。
具体案例:某电商大促期间,同一商品行被 1000 个并发订单更新,InnoDB 实时检测到等待环后,自动回滚 undo 量最小的那个事务,应用捕获 1213 死锁错误并自动重试 3 次,最终只有约 0.3% 的请求需要重试,整体下单成功率保持在 99.5% 以上。
金融系统双向转账
阶段:事务回滚 + 受害者选择
解决的问题:账户 A 向 B 转账、B 向 A 转账同时发生时,两个事务分别先锁住对方要用的账户行,形成互相等待。数据库死锁检测会选择回滚代价最小(已更新行数少)的事务,让另一个事务顺利完成,避免两个账户的资金操作互相卡死。
具体案例:某支付平台在压测“用户间转账”接口时发现死锁率约 2%,定位后发现是两台应用服务器上的事务加锁顺序不一致。团队统一约定“按账户 ID 升序加锁”后,死锁率降至 0.01%,同时打开死锁日志持续监控,问题彻底闭环。
DBA 死锁监控与优化
阶段:日志分析 + 等待图复盘
解决的问题:生产环境偶尔出现“事务等待超时”告警,但不知道是谁和谁在互相等待。通过 SHOW ENGINE INNODB STATUS 查看最近一次死锁的等待环,把每条 SQL 涉及的加锁表与行数画出来,就能定位到“事务先锁 A 表再锁 B 表,另一个事务先锁 B 表再锁 A 表”的顺序冲突。
具体案例:某数据中台团队在每周巡检中,通过死锁日志发现两张宽表之间的关联更新经常成环,随后在代码规范中增加“统一先锁主表、后锁明细表”的要求,并给异常检测追加了死锁率趋势告警,三个月内死锁从每周 20 次降到 0。
Python 演示
class DeadlockDetector: """使用等待图检测死锁,并选择最小代价事务回滚。""" def __init__(self): self.wait_graph = {} # 等待图: txn_id -> set of holder_ids self.transactions = {} # 事务元数据: txn_id -> {"age","work","locks"} def add_txn(self, txn_id, age, work, locks): self.transactions[txn_id] = {"age": age, "work": work, "locks": locks} self.wait_graph.setdefault(txn_id, set()) def add_wait(self, waiter, holder): self.wait_graph.setdefault(waiter, set()).add(holder) def remove_wait(self, waiter, holder): self.wait_graph.get(waiter, set()).discard(holder) def detect_cycle(self): visited = set() path = [] def dfs(node): visited.add(node) path.append(node) for nxt in self.wait_graph.get(node, ()): if nxt not in visited: if dfs(nxt): return True elif nxt in path: idx = path.index(nxt) self.cycle = path[idx:] + [nxt] return True path.pop() return False for txn in list(self.wait_graph): if txn not in visited: if dfs(txn): return self.cycle return None def choose_victim(self, cycle): # 回滚代价 = 已完成工作量 + 已持锁数量,越小越划算 def cost(txn_id): t = self.transactions[txn_id] return t["work"] + t["locks"] return min(cycle, key=cost) def resolve(self): cycle = self.detect_cycle() if not cycle: return None victim = self.choose_victim(cycle) # 回滚受害者:释放它持有的资源,并删除与它相关的等待边 del self.transactions[victim] self.wait_graph.pop(victim, None) for holder_set in self.wait_graph.values(): holder_set.discard(victim) return victim def simulate(): d = DeadlockDetector() # age, work, locks 分别代表运行时长、已更新行数、已持锁数量 for i, age in enumerate([10, 20, 30, 5], start=1): d.add_txn("T%d" % i, age, work=i * 2, locks=1) # 构成环: T1->T2->T3->T4->T1 d.add_wait("T1", "T2") d.add_wait("T2", "T3") d.add_wait("T3", "T4") d.add_wait("T4", "T1") cycle = d.detect_cycle() print("检测到死锁环:", " -> ".join(cycle) + " -> " + cycle[0]) victim = d.resolve() print("选择回滚受害者:", victim) print("回滚后等待边:", {k: sorted(v) for k, v in d.wait_graph.items() if v}) if __name__ == "__main__": simulate()
self.wait_graph.setdefault(waiter, set()).add(holder)记录一条等待边:等待者指向持有者,对应白板上新增一个箭头。elif nxt in path:DFS 时发现邻居已经在当前递归路径里,说明绕回了自己,这就是环。self.cycle = path[idx:] + [nxt]从递归路径中截取从环起点到终点的一段,得到完整的死锁环。return min(cycle, key=cost)在所有环内事务里挑代价最小者,对应停车场管理员请“损失最小”的车退场。holder_set.discard(victim)受害者回滚后,从所有其他事务的等待集合中移除它,等待图不再指向它,死锁环随即断裂。
常见误区
误区事务卡住了就是死锁,SQL 执行慢、一直等锁、报锁等待超时,我都叫它死锁。
正解普通锁等待是一个事务在等另一个事务释放锁,只要对方释放就能继续,是“排队”;死锁是多个事务互相持有对方需要的锁,形成环形等待,靠排队永远等不到。停车场里一辆车挡路是阻塞,四辆车围着互相挡路才是死锁。
为何常见:两者的表象都是“卡住不动 + 锁等待超时”,初学阶段很难从现象区分本质。
误区等待超时实现简单又不用画图,是不是比死锁检测更高效?很多数据库默认用超时不就够了?
正解等待超时的最大问题是误杀——一个正常的大事务可能只是排队等得久,却被当成死锁回滚。等待图检测能精准识别“环”这个死锁标志,不会冤枉好人,所以被 InnoDB、PostgreSQL、SQL Server 等主流数据库用作主检测手段,超时只作兜底。
为何常见:超时机制直观“好想”,而等待图需要理解图和 DFS,初学者容易低估误杀的代价。
误区死锁影响这么大,那我把检测间隔调到最小,每秒检测几千次,不是能第一时间发现死锁吗?
正解每次环检测都要遍历等待图的顶点和边,总开销是 $f \cdot O(V+E)$。检测频率过高会占用大量 CPU,在高并发下反而拖垮数据库吞吐。正确的做法是根据死锁发生频率动态权衡,必要时甚至关闭实时检测,依赖锁等待超时兜底。
为何常见:只看到死锁的破坏性,忽略了检测本身也有成本,忘了“监控不能影响业务”这个运维常识。
误区死锁发生后,数据库回滚的只是报错的那条 SQL,我之前做的其他修改应该还保留着吧?
正解数据库通常以事务为原子单位,死锁引发的回滚大多是整个事务回滚,意味着这个事务之前所有已执行的修改全部撤销。只有显式设置保存点(Savepoint)并回滚到保存点的场景,才能保留部分工作。
为何常见:受“单条 SQL 报错只影响该语句”的经验影响,忽略了事务的原子性边界。
误区既然数据库能检测死锁、自动回滚,那我在代码里什么都不用做,等它处理完不就行了?
正解数据库只负责“回滚其中一个事务”来解除死锁,但被回滚事务的业务逻辑并不会自动重试。应用层必须捕获死锁异常(如 MySQL 的 1213 错误),做有限次重试,否则用户会看到“操作失败”。另外,死锁率过高时,优化事务的加锁顺序才是治本。
为何常见:把“数据库能自动回滚”误解为“数据库能自动完成业务”,忽略了应用与数据库的分工边界。
面试题
Q1. 死锁的四个必要条件中,“占着自己的资源,还等着别人的资源”描述的是哪一条?
- A.互斥条件
- B.持有并等待条件← 正确答案
- C.不可抢占条件
- D.循环等待条件
答案:B。持有并等待是指事务已经持有一个资源,又在申请另一个资源。互斥指资源同一时刻只能被一个事务占有;不可抢占指资源不能被强行夺走;循环等待指多个事务形成环形等待链。题目描述的是“占着 + 等着”,正是持有并等待条件,故选 B。
Q2. 等待图检测死锁时,判断死锁成立的核心标志是什么?
- A.图中存在一条边
- B.图中存在有向环← 正确答案
- C.图中顶点数量超过四个
- D.图中出现双向箭头
答案:B。等待图中有向环意味着事务们互相等待、形成闭环,谁也无法推进,这就是死锁的充要判据。而一条边只是单次等待关系,顶点多或双向箭头都不等于死锁,故选 B。
Q3. MySQL InnoDB 默认的死锁处理方式是?
- A.双方都回滚
- B.回滚最小事务← 正确答案
- C.重启死锁线程
- D.等待超时解决
答案:B。InnoDB 默认开启死锁检测(innodb_deadlock_detect=ON),检测到环后选择回滚 undo log 最少的事务,即“回滚代价最小”的事务,而不是双方都回滚。等待超时只是兜底机制,故选 B。
Q4. 使用等待超时策略时,最大的缺点是?
- A.检测速度过慢
- B.容易误杀长事务← 正确答案
- C.消耗额外内存
- D.无法解除死锁
答案:B。等待超时只看“等了多久”,不看是否真的成环。一个正常的长事务可能因为排队等锁超过阈值就被强制回滚,造成无辜误杀。它本身能解除僵局,也不消耗额外内存,缺点就是误判成本高,故选 B。
Q5. 选择死锁回滚受害者时,通常优先回滚哪类事务?
- A.持锁数量最多的事务
- B.回滚代价最小的事务← 正确答案
- C.启动时间最早的事务
- D.连接序号最小的事务
答案:B。死锁处理的目标是让整体损失最小,所以选择回滚代价最小的事务——通常是已完成工作最少、undo 最少的事务。持锁最多、启动最早、序号最小都不是数据库选择受害者的依据,故选 B。
Q6. 关于回滚粒度,以下说法正确的是?
- A.所有数据库死锁都只回滚出错语句
- B.回滚到保存点保留之前工作← 正确答案
- C.部分回滚会释放全部锁资源
- D.保存点用于加速死锁检测
答案:B。保存点(Savepoint)允许事务回滚到某个中间位置,保留该位置之前的工作,这是“部分回滚”的价值所在。数据库死锁默认回滚整个事务而非单条语句;部分回滚只释放保存点之后的锁,不会释放全部资源;保存点与死锁检测速度无关,故选 B。
Q7. 设等待图顶点数为 V、边数为 E,用 DFS 做一次完整环检测的时间复杂度是?
- A.O(V)
- B.O(E)
- C.O(V+E)← 正确答案
- D.O(V×E)
答案:C。深度优先搜索遍历整个有向图时,每个顶点访问一次、每条边检查一次,因此单次环检测的复杂度是 O(V+E)。O(V) 只算了顶点忽略边,O(E) 只算了边忽略顶点,O(V×E) 是错误的高估,故选 C。
Q8. 下列哪些措施可以有效减少应用中的死锁?
- A.所有事务按相同顺序访问资源← 正确答案
- B.尽量缩短事务执行时间← 正确答案
- C.提高隔离级别到串行化
- D.将大事务拆分为多个小事务← 正确答案
答案:ABD。统一资源访问顺序打破循环等待,缩短事务减少持锁窗口,拆分大事务降低多资源冲突概率,三者都能降死锁。而将隔离级别提高到串行化相当于全表互斥,只会增加阻塞和死锁风险,不能减少死锁,故选 A、B、D。
Q9. 关于等待图死锁检测机制,下列说法正确的有?
- A.能精准发现环形等待← 正确答案
- B.需要维护等待关系边← 正确答案
- C.检测本身有 CPU 开销← 正确答案
- D.可以完全避免死锁发生
答案:ABC。等待图检测在事务请求锁时加边、释放锁时删边,通过找环精准识别死锁;检测需要遍历图结构,自然消耗 CPU。但它只是“发现者”而非“预防者”,死锁仍可能发生且需要回滚处理,不能完全避免,故选 A、B、C。
Q10. 关于死锁与活锁的区别,以下说法正确的有?
- A.死锁形成循环等待环← 正确答案
- B.活锁是让出锁导致饥饿← 正确答案
- C.死锁需要干预才能解除← 正确答案
- D.活锁也总是形成闭环
答案:ABC。死锁是环形等待、互不相让,必须通过回滚干预;活锁是事务反复被让出资源,永远轮不到执行,表现为“饥饿”但并没有形成闭环。活锁通常由调度策略或锁升级机制引发,比如不断被高优先级事务抢占,故 D 错误,选 A、B、C。