读完这篇,你能做什么
能让两个工作进程竞争同一任务,并通过故障注入核对远程操作结果与本地状态是否一致。
学习理由、准备要求与知识点
为什么学它
网络出错或多个进程同时恢复时,同一操作可能再次到达。重试、租约和幂等需要一起设计,才能处理重复与争抢。
先读这些内容
知识点清单
- 可重试与不可重试错误
- 指数退避和抖动
- 整体截止时间
- 多层重试放大
- 稳定幂等键和结果核对
- 任务租约与所有权
- 版本条件更新
- 并发恢复故障窗口
运行前先确认完整算法使用 Java 17 标准库;JDK 源码基线为 OpenJDK 17 GA。示例是内存中的机制模型,外部服务接入需要另外配置和验证。
01先弄懂它是什么
请求失败后再试一次,有时能跨过临时故障,但会额外消耗时间和请求。网络中断、限流或暂时服务错误可能适合重试;参数不合法、权限被拒绝,通常要先改变条件。超时更要小心:它只说明调用方还不知道结果,服务端可能已经把操作做完了。
幂等要求同一业务请求重复执行时,与执行一次具有相同的预期业务效果,不保证代码只运行一次。实现时可以利用操作本身的性质,也可以用幂等键识别重复请求并复用已记录结果。必须说清键在哪个范围内有效、怎样比对请求内容、记录保留多久。多个工作进程还可能同时重试同一任务,因此也要用租约、版本或条件更新协调执行。
多层重试会放大请求。外层三次、SDK三次、工具内部三次,最坏可能变成二十七次尝试。任务应共享总截止时间与总调用预算,选择一个主要重试协调层,避免每层独立把局部上限当作全局上限。
文中这几个词,先认一认
- 指数退避
- 连续失败后逐渐把等待时间按倍数加长,同时设置最长等待,避免一直密集重试。
- 抖动
- 在重试等待时间里加入受控的随机变化,让很多客户端不至于同一时刻再次请求。
- 租约与围栏
- 租约给执行者一段时间内的临时执行权;围栏编号还必须由资源端核验,拒绝小于已接受最新编号的请求。只生成一个递增编号,并不能自动拦住旧执行者。
02看看它怎么工作
本题先演示确定性退避计划:基础等待二、上限八、等待预算十四,依次安排二、四、八,总和十四;下一次已经没有预算。实际调用耗时不在这份纯等待计划内,生产应在每次失败后用当前截止时间重新计算剩余时间。
如果服务端明确要求至少等待五,而当前退避是二,则等待五;服务要求可能超过本地退避上限,此时要么遵守且仍在整体截止内,要么终止,不能为了满足本地cap提前重发。协议如何表达重试建议必须按具体接口核对。
翻倍前检查是否会超过上限,使用剩余预算比较代替先累计,避免long溢出。抖动可在确定性计划之上按定义加入,但不能突破服务端最低等待和整体截止。重试前仍需核对操作是否可安全重复,调度算法本身不提供幂等。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart TD
F["调用失败或结果未知"] --> C["错误与幂等分类"]
C --> B["共享截止和尝试预算"]
B --> D["退避 抖动 服务建议"]
D --> O["所有权与版本核对"]
O --> R["安全重试"]
B --> E["预算耗尽则终止核对"]按顺序看,每一步做了什么
- 按错误类型和业务动作判断是否具备安全重试条件。
- 共享任务截止时间、总尝试数与幂等操作键。
- 结合退避、服务端等待建议和抖动计算下一次时间。
- 执行前核对租约或版本,拒绝过期工作者写入。
- 达到预算或结果仍未知时停止自动重试并核对状态。
03对照实现代码
OpenJDK 17 GA,jdk-17+35 · Duration.multipliedBy / toMillis
下方为原创 Java 教学片段,调用已核验的 JDK API 说明局部机制;不是该类原样源码,也不实现神经网络。完整程序见算法模块。
Duration next = current.multipliedBy(2); // 放大等待时间时可能发生算术溢出。
if (next.compareTo(cap) > 0) next = cap;
long millis = next.toMillis(); // 转毫秒也有范围边界,不能假设任意Duration都可表示。
// 正式调度应先限制范围;算法模块展示不会先翻倍溢出的饱和计算。Duration提供带单位的时间量,multipliedBy执行倍数运算并在无法表示时报告算术错误。使用时间类型减少单位混淆,但不自动消除范围问题。
先计算再取最小值可能在取上限之前已经溢出。完整算法先比较cap与当前值的剩余差,再决定翻倍或直接饱和到cap。
toMillis转换同样可能因范围过大出错,毫秒、秒和纳秒不能混用。应用配置应明确单位,并验证合法范围与整体截止时间。
JDK时间运算只构造等待量,不发送请求,也不知道错误是否可重试。业务幂等、限流语义和工作者所有权需要在外层结合。
代码先调用谁,再调用谁
- 错误分类与剩余截止时间
- 计算当前退避量
- 按范围安全翻倍并封顶
- 尊重服务端最低等待
- 决定重试或终止
04动手写一个例子
截止预算内的安全退避计划
输入正base、cap不小于base、非负等待预算及每次服务端最低等待,返回能安排的等待序列;不实际sleep或发请求,不计调用耗时。Java17 main运行边界自测。
- 输入 1
base2,cap8,budget14,5次最低等待均0- 输出 1
[2,4,8]
- 输入 2
base2,cap8,budget9,最低等待5/0- 输出 2
[5,4]
先验证所有服务端最低等待非负,即使某次因预算不足不会执行,也不把非法配置藏在未访问尾部。
每轮等待取本地退避与服务最低值较大者,若超过剩余预算就停止;否则加入并扣减。这样累计等待始终可表示且不超过预算。
退避翻倍采用饱和计算:当current大于等于cap-current时直接取cap,否则翻倍。此时加倍结果小于cap且在long范围内,不需要先溢出再补救。
为什么这样写是对的已安排等待总和不超过初始预算,left等于剩余额度;current始终位于[base,cap],下一次等待不早于服务端要求。
r为允许重试次数,也即最低等待数组长度;每项校验并至多处理一次,返回序列最长r。真实请求耗时与并发调度不包含在此结论中。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
import java.util.*;
public class Main {
static List<Long> schedule(long base,long cap,long budget,long[] serverMinimum){
if(base<=0||cap<base||budget<0)throw new IllegalArgumentException("config");
for(long x:serverMinimum)if(x<0)throw new IllegalArgumentException("server delay");
List<Long> waits=new ArrayList<>();long current=base,left=budget;
for(long minimum:serverMinimum){
long delay=Math.max(current,minimum); // 不早于服务端最低等待,即使超过本地退避cap。
if(delay>left)break;waits.add(delay);left-=delay; // 先比较剩余量,避免累计溢出。
current=current>=cap-current?cap:current*2; // 饱和翻倍,不先做危险乘法。
}
return waits;
}
static void eq(List<Long> e,List<Long> a){if(!e.equals(a))throw new AssertionError(a);}
public static void main(String[] args){
eq(List.of(2L,4L,8L),schedule(2,8,14,new long[5]));
eq(List.of(5L,4L),schedule(2,8,9,new long[]{5,0}));
eq(List.of(),schedule(1,8,0,new long[]{0}));
eq(List.of(),schedule(1,8,9,new long[]{}));
eq(List.of(Long.MAX_VALUE),schedule(Long.MAX_VALUE,Long.MAX_VALUE,Long.MAX_VALUE,new long[]{0,0}));
try{schedule(1,8,1,new long[]{0,-1});throw new AssertionError();}catch(IllegalArgumentException expected){}
System.out.println("PASS 6 tests");
}
}这些特殊情况也要试一试
- 服务端最低等待超出剩余截止时停止,不提前重发。
- 预算只覆盖等待,真实执行后必须重新计算剩余时间。
long极值不能先翻倍再Math.min。
进阶选读 · 后续再读:分布式恢复与围栏令牌
场景旧工作者租约过期后恢复运行,与新工作者同时写结果。
做法租约获取时分配递增围栏号,受保护资源拒绝旧号写入;幂等键绑定业务操作,条件更新绑定任务版本,并核对远端结果。
复杂度协调读写成本取决于存储与网络;围栏只有在资源实际检查时有效,不能用本地布尔标记保证跨系统互斥。
好处与代价租约提高恢复能力但有过期窗口;围栏和幂等增加协议复杂度,却能明确拒绝陈旧执行者。
05项目里怎么用
在一个位置集中记录尝试次数、退避原因与最终状态,关闭或协调下层自动重试。一次任务失败应能解释总共发出多少请求、哪些结果未知和为什么停止。只记录最后一次异常会掩盖重试放大与重复副作用。
幂等记录保存请求摘要和结果,同键不同参数应拒绝,记录期限覆盖可能重放窗口。超时后先查已存在结果,无法确定时进入核对流程。幂等键不能随每次重试重新生成,否则服务端无法识别同一业务操作。
并发测试要模拟:旧工作进程暂停太久,租约已过期;新进程接手后,旧进程又醒来继续写。围栏号就是每次接管时递增的号码,写入请求要把它带给真正保存数据的资源;资源若已接受更大的号码,就应拒绝后来到达的旧号码。还要测试两个消费者同时接手和结果提交冲突,不能只看调度表里的 owner 是否唯一。远端服务若不检查围栏号,这层保护就没有延伸到它,仍需该服务自己的幂等机制和结果查询。
容易踩的坑
- 参数和权限错误也进行指数重试。
- SDK、业务层和任务层各自重试造成乘法放大。
- 每次重试生成新幂等键。
- 租约过期就假设旧工作者已经停止。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| 集中重试协调 | 总预算清晰,便于诊断 | 需控制下层默认策略 | 多层调用链 |
| 指数退避加抖动 | 降低故障期间同步冲击 | 延迟变长且需可重复测试 | 暂时故障与限流 |
| 围栏与条件更新 | 能拒绝陈旧执行者 | 要求资源实际参与检查 | 多工作者恢复和写入 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01超时为什么不等于失败?
展开参考答案与推导
客户端缺少完成证据,服务端可能已执行;结果未知必须核对,不能自动按未执行重发。
02抖动解决什么?
展开参考答案与推导
减少大量客户端在同一时间重试的同步冲击,不提供授权、幂等或事实正确性。
03租约与围栏区别是什么?
展开参考答案与推导
租约分配暂时执行权,围栏号让资源拒绝旧持有者;只有调度层知道租约并不能阻止旧进程继续写。
换个情况,还会处理吗
01服务建议等待一分钟但任务只剩十秒,怎么办?
展开参考答案与推导
停止这次自动重试并返回明确状态,不能提前十秒重发违反建议,也不能悄悄超出任务截止。
02三层各重试三次,最坏多少调用?
展开参考答案与推导
在完全嵌套且每层耗尽时可达二十七次;实际需检查层间触发规则并集中总预算。
03同键不同参数收到旧结果,哪里有缺陷?
展开参考答案与推导
幂等记录缺少请求摘要比较;应拒绝冲突,避免旧结果被误认为当前操作结果。
写代码试一试
01极值退避怎样避免溢出?
- 输入 / 条件
current=cap=Long.MAX_VALUE- 输出 / 预期
仍为cap
提示 先比较cap-current。
展开参考答案与推导
判断已达到饱和条件就直接赋cap,不执行current*2;Math.min不能修复已经溢出的乘法。
02为什么验证未执行的尾项?
- 输入 / 条件
预算1,最低等待[0,-1]- 输出 / 预期
拒绝配置
提示 未访问不代表合法。
展开参考答案与推导
先完整校验数组,避免非法数据因为当前预算恰好不足而暂时隐藏。
03计划包含请求耗时吗?
- 输入 / 条件
等待2秒,请求又耗3秒- 输出 / 预期
实际剩余再减5秒
提示 输入模型只计等待。
展开参考答案与推导
正式调度每次依据当前单调时间与截止重新计算,不能把预先计划的等待总和当成完整任务耗时。
读完之后,独立完成
课后作业:自己做一遍
本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能让两个工作进程竞争同一任务,并通过故障注入核对远程操作结果与本地状态是否一致。
本篇重点加练
整体截止时间与执行所有权共同约束恢复任务,重点验证时钟和租约变化下的行为。
- 整体截止时间
- 任务租约与所有权
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01原理问答
将认证失败、限流、短时断网与业务拒绝分类,设计带抖动的重试政策,并说明判定所需的状态与响应信息。
- 提交内容
- 一份独立书面回答或标注图,包含自造例子、概念边界及判断依据。
- 怎样算完成
- 逐项回应题目要求,区分事实、假设与个人判断。
- 能用自己的例子解释适用条件,记录尚未弄懂的点。
练到的知识点可重试与不可重试错误 · 指数退避和抖动
- 作业 02代码推演
客户端、任务层和SDK各最多尝试3次,总截止时间2秒。先自行声明每次调用耗时、各层退避与截止时间传播规则,再推演持续失败的调用树,逐步核算次数和剩余时间。
- 提交内容
- 手工推演记录,写明输入约定、中间状态、最终判断;可运行时附程序对照记录。
- 怎样算完成
- 推演包含中间过程,数据形状、单位、顺序或状态前后一致。
- 主动检查至少一个边界或异常条件,能解释手算与运行差异。
练到的知识点整体截止时间 · 多层重试放大
- 作业 03原理问答
为持有租约的任务定义截止时间、续租时机和停止执行的合同,区分调度时间、实际执行时间与观测到的租约状态。
- 提交内容
- 概念或合同说明、关系图与自造案例,明确事实、假设和适用条件。
- 怎样算完成
- 关键概念有明确边界,能用案例检验声明。
- 未获证据支持的假设单独标注。
练到的知识点整体截止时间 · 任务租约与所有权
综合应用
- 作业 04面试场景
两个工作进程同时恢复一项写操作,其中一个租约已过期但仍在执行。设计幂等标识、所有权检查和结果核对流程。
- 提交内容
- 场景分析与决策记录,包含信息缺口、约束、备选方案和验证计划。
- 怎样算完成
- 方案回应给定约束,列出至少一种替代方案及代价。
- 每项关键结论说明所需证据,不能核实的部分明确标注。
练到的知识点稳定幂等键和结果核对 · 任务租约与所有权
- 作业 05编程实践
编写并发恢复模拟器,用可控制时钟与版本更新制造“已执行未记结果”的窗口,测试重复调度、旧所有者返回和任务终止。
- 提交内容
- 可复现的最小程序或实验材料、自写测试、环境说明与运行记录;题目允许纸面实验时提交完整案例表。
- 怎样算完成
- 提交物按说明可以重现;测试覆盖正常、边界与失败场景。
- 运行证据与断言能核验声明的行为,注明模拟与真实执行范围。
练到的知识点版本条件更新 · 并发恢复故障窗口
- 作业 06排错实验
故意在每次重试时更换幂等键,记录副作用与请求数量;再检查超时未知是否被错误计为明确失败。
- 提交内容
- 最小故障输入、复现步骤、观测记录、定位依据及回归用例。
- 怎样算完成
- 其他人可按记录复现故障,并区分现象与已验证原因。
- 定位过程有观测依据,回归用例可以再次暴露原问题。
练到的知识点指数退避和抖动 · 整体截止时间 · 稳定幂等键和结果核对
- 作业 07编程实践
用可控制时钟实现两个工作进程的离线模拟,分别注入任务超过截止、续租延迟和旧进程迟到返回,检验实际允许执行的时间区间。
- 提交内容
- 可复现的小程序或题目允许的离线实验、自写测试、输入和运行记录。
- 怎样算完成
- 输入、环境和运行过程可重现,测试覆盖声明的重点条件。
- 验证目标由合同确定,模拟结果不冒充真实服务证据。
练到的知识点整体截止时间 · 任务租约与所有权
- 作业 08排错实验
模拟一个进程根据本地时间判断仍可执行,另一个进程已取得新租约;记录时间与所有权证据,设计可以识别重叠执行的断言。
- 提交内容
- 最小故障序列、观测记录、原因核对与可再次触发原问题的测试。
- 怎样算完成
- 故障可以稳定重现,现象与已验证原因分开。
- 观测记录足以判断声明的合同是否被违反。
练到的知识点整体截止时间 · 任务租约与所有权
进阶挑战 · 掌握前置后选做
- 作业 09设计评审
为不同层的重试职责做设计评审,给出统一预算、观测字段和故障演练方案,说明模拟无法验证的真实服务承诺。
- 提交内容
- 设计评审文档或对照实验报告,包含前置条件、方案取舍、验收证据与尚未验证的范围。
- 怎样算完成
- 比较使用一致条件与事先声明的标准,记录失败与未知结果。
- 说明方案的适用前提、未覆盖风险及后续验证方法。
练到的知识点多层重试放大 · 任务租约与所有权 · 版本条件更新
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 条件变化
如果固定时长任务变为耗时不可预测的任务,重新评审截止、续租与取消政策,说明改善可用性时需要额外承担的重复执行风险和验证成本。
- 提交内容
- 变更前后条件表、重新评审的方案、检验用例与新增代价。
- 怎样算完成
- 只改变明确列出的条件,比较口径保持一致。
- 修改方案说明收益、代价与仍未验证的范围。
练到的知识点整体截止时间 · 任务租约与所有权
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读实践指南英文
Google Cloud:Retry strategy (新标签页)↗
重点阅读指数退避、幂等条件与重试反模式;示例针对 Cloud Storage,应用到其他服务时须重新核对错误语义。
- 实践参考文档英文
PostgreSQL:数据约束 (新标签页)↗
阅读主键、唯一约束、外键与检查约束,用数据库约束验证重复事件和幂等记录。
- 深入参考文档英文
PostgreSQL:事务隔离 (新标签页)↗
阅读并发事务、可见性与序列化失败处理,设计版本条件更新并验证竞争情况。
- 深入实践指南英文
LangGraph:Interrupts (新标签页)↗
阅读暂停恢复和节点重新执行的限制,把确认与副作用分开,并用故障注入检查幂等性。
- 深入参考文档英文
OpenJDK 17 Duration 源码 (新标签页)↗
按指定版本定位
Duration.multipliedBy/toMillis;对照源码分析理解实际调用边界。