一笔钱处理了两遍
老王复盘一笔诡异资损:支付服务调渠道超时(超时设 5 秒),按失败处理给用户发了退款;渠道那边其实在第 6 秒扣款成功了。结果渠道扣了一笔,平台退了一笔——超时被当成了失败,这是分布式系统最经典也最贵的一类误判。本篇把超时与重试的纪律一次讲清。
超时是三态:成功、失败、未知
第 1 篇立过规矩:远程调用有第三态。超时永远不等于失败——超时只说明「响应没在约定时间回来」,不说明「业务没执行」。请求可能还在下游队列里排着,可能已经执行成功只是响应包丢了。把超时当失败的每一行代码,都埋着一笔双重处理的雷。
未知态的处理纪律只有一条:不猜,先查询。超时后调下游的查询接口(按流水号查这笔处理到哪了):查到成功,按成功走;查到失败,按失败走;暂时查不到,保持未知状态挂起,定时再查——实在查不出来的,进对账系统兜底(第 16 篇)。宁可慢,不可错。
超时时间怎么定
太短——下游正常处理就被误判,退款的雷一个接一个;太长——上游线程被吊住,故障时连接池被拖垮(第 14 篇分布式 ID 高可用的教训同源)。经验值:取下游正常耗时 P99 的两到三倍,并区分连接超时(建连,几百毫秒)与读超时(等响应,按业务)。更重要的是监控超时率——超时率突增说明下游病了,该触发的是熔断与告警,而不是让每个请求都傻等满超时。
重试的四条纪律
纪律一:只重试「确定没生效」的请求。连接被拒、DNS 失败这类「请求根本没到」的失败,可以放心重试;超时类的未知态,重试之前先走查询确认——除非操作天然幂等且业务容忍重复(查询类请求随意重试)。
纪律二:重试的前提是幂等。第 10 篇的五大武器就是为这一刻准备的——没有幂等兜底的重试,等于主动制造重复扣款。
纪律三:指数退避加抖动。失败后等 100 毫秒再试、再失败等 200、400……间隔翻倍,且每次加入随机抖动(比如在退避值上浮动 20%)。没有抖动的固定间隔重试,会让所有实例在同一毫秒齐射——重试风暴:下游抖了一下,全体上游的 QPS 瞬间翻倍,本来能自愈的小故障被重试压成彻底瘫痪。
纪律四:设重试预算。单链路重试次数封顶(比如最多两次),全服务重试流量占比封顶(比如 10%)——超预算直接快速失败。重试是放大器,没有预算的放大器迟早烧掉整个系统。
一次正确的超时处理
调渠道扣款 → 读超时(5s,P99 的 2.5 倍)
↓ 未知态,不猜
按流水号查询渠道 → 返回「处理中」
↓ 保持挂起,30 秒后定时再查
返回「成功」→ 落成功状态,业务继续
(若最终查不到 → 挂起单进对账,人工兜底)熔断:比重试更高一级的刹车
超时率持续走高时,重试只是给病人做心脏按压,熔断才是停止压迫先止血:错误率过阈值后一段时间内直接快速失败(不再傻等超时),半开状态探活恢复后才放流量回来。配合舱壁隔离(按下游隔离连接池,一家故障不拖垮全部),就是 resilience4j、Sentinel 这类组件的日常工作。分布式事务链路上,每个远程跳点都要有超时、重试、熔断三件套——少一件,故障就会顺着调用链放大。
小结
超时重试一句话:超时是未知态不是失败,先查询后行动;重试四纪律——只重试确定未生效、幂等打底、退避加抖动、设预算;熔断是高一级的刹车,三件套缺一不可。链路上单点的纪律齐了,但很多事故的根不在远程调用,而在本地事务里偷偷干了不该干的事——下一篇:@Transactional 的七宗罪。
评论 (0)