Skip to content

算法Algorithm

Wait-die与Wound-wait死锁预防

Wait-die · Wound-wait · Timestamp-based deadlock prevention

用稳定唯一年龄给事务锁等待定向,分别追踪主动回滚与被迫清理,说明多持有者、升级和公平边界。

形式陈述 ​

年龄决定谁可等待 ​

给每个逻辑事务T分配唯一年龄ts(T),值越小越老;不同重试保留同一个值。可用单调计数器加唯一编号,年龄不要求是真实时间。事务遵守严格两阶段锁,锁请求由一个顺序的抽象管理器处理;提交、伤害标记、释放彼此有明确原子次序。

请求者T遇到其它事务U持有不兼容锁时,两种策略为:[1, §3.11]

策略 T比U老 T比U年轻
wait-die T等待U T中止自身
wound-wait 标记U中止,T等其清理 T等待U

这里“伤害”是请求持有者进入ABORTING,不是直接把锁改名给请求者。若持有者已经提交并释放,重新检查即可;不能撤回已确认的提交。本文模型把提交与进入ABORTING的选择串行化,ABORTING之后禁止普通操作和提交。

每个事务至多有一个待完成锁请求。等候者不靠额外FIFO前驱阻塞:能否授锁只取决于当前不兼容持有者,未获锁请求不会充当锁持有者。若另加排队优先约束,必须把那些额外等待边也纳入证明,不能只给持有锁的边检查年龄。

持有集合不是静止的。每次授锁或模式转换可能给已有待请求增加新的阻塞者,因此管理器在该原子事件返回前,必须重新检查受影响待请求,并先触发年龄规则要求的中止;不能只在第一次决定等待时检查一次。本文核验器保守地重查所有待请求,反复到没有新中止标记,再允许事务继续普通操作。每次新标记都会减少可执行的事务数,所以有限事务集合上这一步能结束。

多个持有者与升级 ​

设B是本次新请求或升级所冲突的其它持有者集合。B为空便授予。wait-die只有当T比B中每个仍活动的事务都老时才允许普通等待;遇到任何更老持有者,就中止T。不能只随意挑一个较年轻持有者检查。

wound-wait标记B中所有比T年轻且仍活动的持有者中止;对更老活动持有者等待。即使被标记者曾在等另一把锁,也先取消它的待完成请求,让它只做清理。清理中的事务仍保有锁直到真正释放;请求者必须重新检查剩余B,不能因所有年轻持有者都收到标记便提前授锁。

升级先把请求模式与自身旧模式合并,再计算B;自身不是自身的阻塞者。若升级要等待,旧模式仍保留。请求中止后,其全部锁也需等清理完成才释放,不能先丢保护再回滚数据。

无环证明包含清理状态 ​

在wait-die中,活动事务间的每条普通锁等待边T→U都满足ts(T)<ts(U)。沿一条环走一周会推出ts(T)<ts(T),不可能。wound-wait中,未被中止的活动事务间每条普通等待边都满足ts(T)>ts(U),同样不可能闭环。这是对死锁的一种预防:在允许阻塞时就限制边方向,而不是先成环再检测。

请求者还可能等待ABORTING事务释放锁,这种边不必遵守同一年龄方向。它仍不会构成锁等待环,因为ABORTING已取消锁请求,不再向别的事务发出锁等待边。这里必须假设清理是有限的,且不会为完成清理再申请本协议中的事务锁;若回滚另需这些锁,证明就缺了一部分依赖。

直觉

两种策略把等待箭头朝相反方向排列 ​

wait-die让老事务耐心等年轻持有者,让年轻请求者回退。wound-wait让老请求者迫使年轻持有者回退,年轻请求者则等待老者。两者都只因这一年龄规则中止年轻的一方,但中止发生在“自己请求时”还是“别人来请求时”,执行轨迹不同。

年龄只处理锁等待。它不决定事务读什么版本,也不自动替代严格2PL;同一组年龄比较若配上错误的提前解锁,仍可能得到不满足隔离的结果。

例子与边界

空区间预订的完整重试 ​

T1年龄10,T2年龄20;两者都已在区间协议中取得S[10,13),并读到空。T1准备插11,T2准备插12。

wait-die轨迹为:

  1. T1请求X[11,12),被T2的S挡住;10<20,登记T1→T2
  2. T2请求X[12,13),被T1的S挡住;20>10,中止T2,而不是再登记反向边
  3. T2清理完成,释放S并移除待请求;重新检查T1的请求,现在可以授予
  4. T1在私有写集中加入11,提交发布后释放锁
  5. T2以原年龄20开始新一轮,重新执行查询;看到11后不再插12,正常完成

重试不是从失败的insert那一行继续。先前“区间为空”的判断已经失效,必须重做决定该写的读取与计算。最终[10,13)只有11;串行解释为T1的成功执行在T2的成功重试之前。

wound-wait则在第1步就将T2标记ABORTING。T1暂时等清理,T2不能再发第2步写请求;直到清理完成才授予T1的X。若实现发标记后立刻授锁,此时T2仍持S,就已破坏兼容不变量,无论后来回滚多快都不能抹去该窗口。

三个年龄和共享锁升级 ​

年龄20的请求者要X(x),而年龄10与30的事务都持S(x)。wait-die必须中止20,因为存在更老的10;只检查30会错误地允许20同时等待10与30,破坏统一方向。wound-wait标记30清理,并继续等10;清理30不会使10的S消失。

若10与20均持S(x),10申请升级X:wait-die让10等20;20再申请升级时中止20。wound-wait在10请求时就标记20。两种过程都应排除10自己的S作为阻塞者,同时保留该S直到X获准或整个事务中止结束。

新持有者也会改变旧等待 ​

wait-die下,20已经在等持S(x)的30,年龄方向合法。此时10也请求S(x),它与30的S相容;但如果直接授予后不重查20,便新增20→10这条反方向等待。正确处理是在这次授予事件返回前中止20。wound-wait的对应例是20先等持S的10,后来30获S;20应将新年轻持有者30标记清理,而不是悄悄容忍20→30成为新的活动事务等待边。

因此“每次锁请求检查一次”还不足以支撑上面的全局不变量:当前请求可能改变别人已经在等待的对象。这个边界由状态核验器的随机授锁历史发现,并保留成两个三事务回归例。

换新年龄可以让重试永远落后 ​

wait-die下,逻辑事务A每次重试都换最新年龄;在其下一次申请x前,总有一个稍早开始的B持X(x)。A总比B年轻,总会中止。每个B都可有限完成,系统也持续取得进展,A却始终无法完成。保留原年龄使后来新事务无法继续变成A的“更老前驱”,因此是避免循环重启分析的重要前提。[1, pp.85–86]

即使保留年龄,若锁持有者永不结束、回滚永久阻塞、可授予请求从不被调度,仍可能无限等待。无等待环不等于无饥饿,也没有给出一个统一毫秒上界。

推论与应用

中止和清理也是协议动作 ​

完整状态至少区分ACTIVE、WAITING、ABORTING、COMMITTED、ABORTED。WAITING不允许继续发下一普通请求;被伤害后取消待请求并转ABORTING;清理完成才转ABORTED并释放全部锁。重试建立新的执行实例,保留逻辑年龄,却不复用旧读结果或未提交写集。

本单元用私有写集使清理可见为“丢弃暂存值再释放”。若真实实现曾原地写共享页,必须在原保护下完成必要undo,再唤醒其它事务;若外部邮件、支付等副作用已经发出,它们也不因数据库事务中止自动撤回。这些动作需要另外的事务边界,不能放进本锁模型的回滚假设中。

保证和成本分别核算 ​

给定g个不兼容持有者,年龄决策扫描它们需O(1+g)次比较;发出wound至多g次。锁表查询、唤醒、丢弃写集和真正undo成本另计。本文下载核验器还在持有者变化后复检所有待请求:若有A个尚未进入ABORTING的事务、P个待请求、L条锁记录,朴素全扫闭环保守为O(1+(A+1)P(1+L)),并非把整个事件都算作单次O(1+g)决策。把“选择中止谁”写成常数,不会让中止一个已修改百万记录的事务也只付常数。

若要进一步证明最终完成,需声明有限事务工作、有限且可运行的清理、合适的公平授锁/CPU调度、每个年龄前只有有限更老事务、重试保留年龄等条件。本文只对有限轨迹验证年龄方向、授锁安全和实际重试结果,不把这些测试当成普遍饥饿自由证明。

终点任务要求逐事件列出B、策略决定、仍持有的锁与可执行状态,并在清理前故意尝试授锁,观察不变量怎样拒绝它。

参考资料

[1] Philip Bernstein、Vassos Hadzilacos、Nathan Goodman,Concurrency Control and Recovery in Database Systems,Chapter3,Addison-Wesley,1987,§3.11的Timestamp-based Deadlock Prevention,印刷pp.84–87,尤其p.86两种策略、重试保留年龄及wound与已完成事务的边界。本文多持有者集合、显式ABORTING和预订轨迹在该规则上展开。

关系图谱8 个相邻概念 · 1 类关系

拖动节点调整位置。

显示关系

显示:依赖

  1. 前置三跳
  2. 前置二跳
  3. 前置一跳
  4. 当前条目
  5. 后续一跳
  6. 后续二跳
  7. 后续三跳
文字版关系按与当前条目的最短距离分组