Java学习随笔:重试之前先想清楚幂等性(2026-10-22)

学习 Java 服务开发时,遇到网络超时,很容易想到再调用一次。这个处理看起来只是多写一段重试逻辑,真正困难的地方却在于:第一次请求究竟有没有完成?调用方没有收到成功响应,只能说明它不知道结果,不能据此断定服务端什么都没做。今天想整理的,就是重试和幂等性之间这层容易被忽略的关系。

可以设想一个创建订单的接口。服务端已经把订单写入数据库,但成功响应在返回途中丢失了。调用方等待超时后重新提交,如果每一次提交都生成新的订单,就会出现两张内容相同的订单。这里的故障发生在响应传递阶段,业务操作本身已经成功。单纯增加重试次数,会把一次通信问题扩大成业务数据问题。因此,在讨论重试之前,需要先定义重复调用应该产生什么结果。

对同一个业务意图重复执行,多次调用产生的业务效果应当与一次调用一致,这是理解幂等性的一个实用起点。这里关心的是约定范围内的业务效果,并不要求每一次响应的时间戳、提示文字都完全相同。重复查询通常比较容易处理,而增加余额、扣减库存、发送通知等操作,需要认真识别重复执行的后果。同一个接口也可能同时包含多个效果,不能只因为数据库里没有重复订单,就认定整个流程都已具备幂等性。

第一步是给业务意图一个稳定标识。这个标识应该在重试时保持一致,能够区分“同一次提交又来了一遍”和“用户确实发起了另一笔订单”。如果服务端在每次收到请求时都新建一个标识,它只能标识每一次到达,无法帮助判断两次到达是否来自同一个意图。标识还要有明确的作用范围,例如所属用户、操作类型和有效期限,避免不同业务之间意外冲突。

不过,识别重复请求不能停留在先查后写。两个请求可能同时查询,都发现记录不存在,然后分别创建订单。单个 Java 进程内的锁只能约束它实际覆盖的执行范围;服务部署成多个实例后,不同实例仍可能并发处理。更可靠的方案需要让存储层参与约束,例如对约定的业务标识建立唯一性约束,并清楚处理竞争失败后的查询和返回。关键是让“只能成功创建一次”成为实际约束,而不是依赖两个步骤恰好没有重叠。

幂等记录和业务数据之间也需要保持一致。如果先写入“已处理”标记,随后创建订单失败,之后的请求可能被错误地当作已经完成;如果先创建订单,再保存标记时失败,重复请求又可能重新创建。两者位于同一个数据库、能够使用同一事务时,可以考虑把相关写入放在同一原子边界内。若跨越多个存储或外部系统,就需要另行设计状态推进、补偿和对账,不能假设一个本地事务能够覆盖所有副作用。

重复请求到来时,还要区分正在处理、已经成功和明确失败。看到相同标识就直接返回“成功”,会掩盖仍在执行或尚未完成的业务。已经成功的请求,可以返回先前保存的结果;正在处理的请求,可以返回约定的处理中状态,或者引导调用方查询;对于失败,则应结合失败发生的位置决定能否继续。还应检查相同标识携带的关键参数是否一致,避免把不同金额或不同商品的请求误认为同一次提交。

重试策略本身也应该有限度。只有那些具备恢复可能、并且重复执行边界已经明确的情况,才适合自动重试。参数不合法通常不会因为再次发送而变正确,持续重试反而浪费资源。对暂时性的故障,可以设定次数和总耗时限制,适当拉开重试间隔,并考虑多个调用方同时重试带来的压力。如果网关、服务和下游各自重试,实际调用量还可能层层放大,所以要明确由哪一层负责,不能只看某一段代码里的次数。

最后,我觉得学习这个主题最有效的练习,是画出请求、写入和响应的时间顺序,在每两个步骤之间假设一次中断,再追问重试会发生什么。特别要覆盖两个相同请求同时到达、成功后响应丢失、处理中进程退出这几种情况。这样思考以后,幂等性就不再只是接口文档里的一个形容词,而是由业务标识、存储约束、状态记录和失败处理共同兑现的承诺。重试能帮助服务恢复,但前提是我们已经想清楚,重复到来的那一次究竟该怎样处理。

posted @ 2026-10-22 11:37  梦幻泡影Qv'Q  阅读(8)  评论(0)    收藏  举报