这份任务对应从局部回退到备份与日志保留。完成时交出的应是一份逐页、逐记录可复查的结果:哪些操作已经被补偿,哪些日志仍可能需要,哪些提交必须保留,哪一种输入必须明确拒绝。
标准库核验器只向stdout写JSON,普通和python -O均执行显式检查。它模拟整数页和持久边界,不创建数据库,不执行真实fsync或删除文件。日志在教学实现中占固定槽,LSN为10、20、30……;真实变长记录需要用其实际长度确认连续区间。
一、保存点不是提交
四页A/P/Q/R初值全0。T的日志如下;S在20后建立,不另写持久记录。
| LSN | 动作 | prevLSN | undoNextLSN |
|---|---|---|---|
| 10 | BEGIN T | 0 | — |
| 20 | A:0→2;随后建立S | 10 | — |
| 30 | Q:0→7 | 20 | — |
| 40 | P:0→9 | 30 | — |
| 50 | CLR把P恢复0 | 40 | 30 |
| 60 | CLR把Q恢复0 | 50 | 20 |
| 70 | R:0→4 | 60 | — |
| 80 | COMMIT T | 70 | — |
按局部回退协议,写完60后T仍ACTIVE,S仍有效,P/Q的锁仍归T。70的前驱必须是60,事务链不能假装这两条补偿未曾发生。若最后commit80可靠完成,结果为A2/P0/Q0/R4。
另走没有commit的分支:稳定日志只到70就崩溃。第一次恢复撤销70,写CLR80,prev70、undoNext60;故意只稳定CLR、不刷补偿页,再次崩溃。第二次恢复先redo CLR80,使R=0,之后游标80→60→20→10;只需新CLR90撤A20,再写END100。两次分支里的LSN80分别是COMMIT和CLR,它们不能混成一条历史。
迁移:在70后建立V,再写P=6,然后回到S。新P和R被撤回,原30/40经CLR跳过,V失效而S保留。紧接着再回S不会重复补偿30/40。若只release S,值不变;释放保存点也不释放事务持有的逻辑锁。
二、明确复制开始前已经稳定的页
在记录30之后、40之前,先可靠刷Q页。此刻磁盘Q7@30,A20仍仅在内存中,所以DPT确为{A:20},而不是漏写Q。取得短原子元数据快照后,事务表含T(first10,last30),下一位置B=40。
计算两种恢复下界:
- r=min(B40,DPT中的20)=20,负责补回落后页
- q=min(r20,T的first10)=10,保留可能需要的整个undo前缀
给出复制事件表。所有复制发生在开始快照以后,读到一个完整稳定页版本;@后面是该版本的pageLSN,不是复制时刻。
| 页 | 复制时已生成日志到 | 镜像 | 说明 |
|---|---|---|---|
| A | 30 | 0@0 | A20仍脏,旧磁盘合法 |
| P | 30 | 0@0 | P40尚未发生 |
| Q | 30 | 7@30 | Q30已在开始前可靠刷出 |
| R | 70 | 4@70 | R70生成后先刷日志与页,再复制 |
复制结束E=70。此时所需日志至少[q10,E70]齐全。先稳定全部副本和这段归档,再发布完成manifest;附件先尝试过早publish并得到拒绝,然后才完成。停止复制不等于备份已经可用。
三、同一备份,两份恢复证书
恢复到T=80,逐条写出redo或skip:20 redo A;30 skip(Q镜像已30);40 redo P9;50 redo P0;60 redo Q0;70 skip(R镜像已70)。T有稳定commit80,无需undo。输出A2/P0/Q0/R4。
用这条运行检查两个错误变体。若只从B40开始redo,A仍为0;若忽略两条CLR,P/Q保留9/7。不要只说“可能出错”,写出实际四页值。
扩展归档:90 BEGIN U;100 U写Q:0→8;110 BEGIN V;120 V写P:0→6;130 COMMIT V。恢复目标130的完整redo先得到A2/P6/Q8/R4。U没有commit,沿100→90撤销并结束,得到A2/P6/Q0/R4;T和V的结果保留。下载结果同时记录before_undo和最终值。
以下输入均应失败,而不是交一份貌似合理的结果:目标60<E70,缺日志60,缺日志120,历史代际不匹配,页面集合不全,manifest未完成。早目标尤其要说明R4已来自70;仅前滚≤60的日志无法生成旧R0。需要更早恢复时,选择更早基线并保留对应完整日志,不能直接修改目标数字。
截掉早前缀后再崩溃
另构两页迁移:OLD10开始、A20写5、COMMIT30并刷A;NEW40开始、B50写9未提交。元数据给q40/r50/B60,复制A5@20和B0@0,E50。丢弃10..30,仅以40/50归档restore并在第一条CLR60后停止。只稳定CLR60,调用resume_restore保留原manifest再入,应redo50、60,沿60跳到40,最后END70,输出A5/B0且不再生成补偿50的CLR。
这里调用restart会得到明确接口拒绝,因为它要求从10开始的完整日志。正确迁移保留的是原镜像、原TT种子和[q40,新稳定末位置],不是重新要求已经淘汰的10..30。再试不稳定CLR60就崩溃:稳定尾仍50,下一次可重新生成60。两种分支都要得到A5/B0。
四、整段日志什么时候可以删除
按保留前沿协议,稳定域为[0,160),段长40。这里160是下一个位置,不是“最后记录编号”;复制/恢复阶段中的E70、目标130则指已包含的末记录。四pin为redo90、undo10、backup10、replica70。
| 事件完成后 | 有效pin | 逻辑R | 实际H | 本次可删段 |
|---|---|---|---|---|
| 全部登记 | 90/10/10/70 | 10 | 0 | 无 |
| undo需要结束;备份独立稳定后取消两pin | 90/70 | 70 | 40 | [0,40) |
| replica持久到100,再推进pin | 90/100 | 90 | 80 | [40,80) |
源日志pin取消不表示唯一归档也能删除。要继续支持第三节的恢复目标130,归档仍须保留该备份的[q10,130];若没有第二份日志来源,就不能按源域的H80销毁唯一副本。
代码会拒绝replica未持久时直接advance(100)。错误变体刻意绕过这个检查,先把pin报到100,再删到80,最后才保存恢复点:在最后一步之前掉电,持久进度仍70,所需记录已丢。枚举D(进度稳定)、P(pin推进)、G(GC)六种次序,写出每步H和持久恢复点;固定各做一次GC的本例中,PGD的G后产生真实缺口。合法实现必须拒绝未经D的P,不靠后来D补救先前承诺。
五、消费者身份与准入迁移
H80时要求从50开始的新消费者,必须明确拒绝。若确有可恢复到80的新基线,可以从80登记;不许只把失败请求的50改成80就宣称补齐了历史。
登记者读到H40之后暂停,GC推进H80,再尝试登记50,原子重检应拒绝;若登记50先成功,则GC最多到40。取消一个名为late的消费者再重建,令牌代际必须变化。用旧令牌读取、推进和宣告完成,三者均被拒绝;新令牌的pin仍留在80。
最后把所有消费者完成/取消,F=160时应允许回收全部四个完整段。再把稳定尾端改为155:逻辑R可以到155,但H最多120,未满的[120,160)不能被当作完整稳定段删除。这是日志域进度与段几何的区别。
六、说明你的证据覆盖到哪里
交付内容至少包含两条保存点分支、四页copy/redo/undo表、两个恢复目标、缺日志和早目标的拒绝理由、三次前沿表及持久顺序反例。保存点测试用原始值快照作oracle,备份测试选择独立页前缀再与提交后的逻辑结果比较,日志保留测试直接构造消费者所需位置集合;不能让三个检查都只复述同一段实现。
本单元假定原子页写、完整日志、可信生产者和可恢复注册表。模型没有实现文件分配、物理损坏修复、跨历史分支选择或分布式未决事务。把这些排除项写清,才知道哪份恢复证书可以复用,哪一步还需要新的机制。