你可以理解为这么一件事:传送带上同一个箱子过了扫码口,扫码枪那一下没"嘀"出声,操作员又扫了一次,系统里就多了整整一单。这就是接口幂等要解决的问题——平台那边网络抖一下、超时没收到回执,按规矩重试三次,仓库这边如果没有防重,就会凭空多出两单。单子是假的,货是真的,拣、发、退,全是真金白银。
先讲原理。幂等的本意很简单:同一个请求发一遍和发十遍,效果一样。工程上落到一个东西:幂等键。平台推单时带一个键,仓库端收到先查这个键有没有处理过——处理过,直接把上次的结果原样返回,不再建单;没处理过,正常建单并把键记下来。你可以理解为箱子上的面单号:扫码口认的是面单号,不是"你扫了几次"。
幂等键设计:三条铁律
键怎么设计,决定了这套东西是真有用还是摆设。三条铁律:第一,键必须有业务语义。最好的键是"平台标识 + 平台订单号",比如 shopify_81204473,一眼能对上业务。最烂的键是随机 UUID——每次重试都换一个新键,等于没做。见过最离谱的,有人拿时间戳当键,重试间隔一秒,键都不一样,防了个寂寞。第二,键的粒度要对。键太粗(比如按店铺加日期),正常的第二单会被误杀;键太细(精确到毫秒),重试的单又对不上。第三,键要有过期时间。键不能永久存,否则几个月后客户真下了个"看起来一样"的单(同平台、订单号复用),会被误判成重复。过期时间按平台的重试窗口设,一般 24 到 72 小时足够——没有平台会几个月后才来重试。
防重做在哪一层:三档选型
选型分界线来了:防重做在哪一层?三档方案。第一档,数据库唯一约束:在订单表上对(平台,平台订单号)建唯一索引。这是最硬的一道墙,并发双写也撞不破,成本几乎为零。缺点是只防"建单"这一步。第二档,缓存键加状态机:用 Redis 之类的存幂等键,建单前先查。好处是能顺手防住"重复流转"(比如同一单被重复标记发货);坏处是多了一个组件要维护,缓存挂了怎么办得提前想好。第三档,平台侧给键:要求上游平台在推单时自带幂等键。这是最干净的,但前提是平台配合——大平台一般都支持,小平台看缘分。分界线很清楚:日单量不大、单体应用,第一档唯一约束就够了,别折腾;多系统、多平台、单量上了规模,直接上第二档;对上游有话语权,能谈下来第三档就谈。
多说一句第二档的预案:缓存挂了怎么办?两条路:降级走数据库唯一约束(慢但不断),或者直接拒绝建单(宁可少接一单,不多发一单)。选哪条看你的业务:自营仓选前者,3PL 给客户代发货选后者——多发一单的赔偿,比少接一单贵得多。这个预案必须在上线前写好,半夜缓存挂了再现想,来不及。
举个复合键的例子:某 3PL 同时接 Shopify 和 Etsy,Etsy 的订单号在不同店铺之间可能重复,键就得是"平台_店铺_订单号"三段拼起来。键里缺了任何一个会撞车的维度,等于白做。上线前把"哪些维度组合起来全局唯一"列成表,一个一个验,别拍脑袋。

案例时间。2024 年大促,某平台在高峰期接口超时,几秒内重试了三次,仓库 OMS 没有唯一约束,连建三个同单号订单,夜班直接发出去了三遍。追回两单,第三单客户拒收退回——来回运费加产品损耗,一单的损失够养这个防重功能好几年。另一个案例更气人:一家 3PL 对接多个平台,用"平台订单号"当键,结果某个平台的订单号在它自己系统里会变(下单时的临时号和支付后的正式号不是同一个),该拦的没拦住,不该拦的拦了半天。教训:键的语义必须跟平台方逐字确认,别想当然。
上线后的三个维护坑
再讲维护的坑,三个。坑一:过期时间设太短。平台延迟重试(比如 48 小时后补推),键已经过期,重复单大摇大摆进来。坑二:只防了建单,没防状态流转。单子没重复,但"发货"接口被调了两次,物流面单打了两张,快递员上门收了两次件——钱照样花出去。坑三:并发双写。两次重试在同一毫秒到达,"查键、没查到、建单"这个序列跑了两遍,两个线程都以为自己是第一个。解法是查和写必须是原子操作,别自己手写"先查后写"。
有句话不吐不快:有人问要不要做"全链路幂等",每个接口都加?我的回答:伪需求。查询类接口天然幂等,加键是脱裤子放屁;只有"建单、支付、发货"这种会改变世界状态的写接口才需要。把劲儿使在刀刃上。
算笔账收尾。一次重复建单的损失:多拣多发的货值、逆向物流运费、客服扯皮的人力,往少了算也是几十块,往多了看小几百。而做幂等的成本:唯一约束半天,缓存方案一到两天。这钱花得值不值,看这一个数就行:过去一年的重复单数量 × 单均损失,超过两天开发成本,就可以动手了。我还没见过算出来不值的。
最后提醒一句:幂等键的过期时间和平台的重试策略要放在一起看。换平台、对接新版本接口时,第一件事就是重新确认对方的重试窗口和键语义——十次防重翻车,九次都栽在"以为对方没变"上。
作者:大刘|自动化设备工程师






