重试之前,先问一句:它幂等吗
接口偶发超时,第一反应是什么?大多数人会写:加个重试。重试确实是分布式系统里最常用的救命手段,但它也是最容易被滥用的。重试一把梭的代码,平时安静得像什么都没发生,出事的时候能把整个系统拖下水。
问题不在于「要不要重试」,而在于很多人跳过了一个更根本的前置问题:被重试的那个操作,幂等吗?
一次真实事故
我见过一个下单接口,客户端 3 秒超时后自动重试。某天数据库抖动,响应慢到了 4 秒。结果就是:第一笔请求其实成功了,只是响应没来得及返回;客户端超时重试,又下了一单。用户付了一笔钱,拿到两份货。
事后复盘,所有人都盯着「为什么数据库会抖」,但真正的问题是:这个接口从设计的第一天起,就没打算被安全地调用两次。
重试永远在放大一个隐含假设——「调用两次和调用一次效果一样」。这个假设有个名字,叫幂等。假设不成立的时候,重试不是容错,是制造故障。
三类操作,三种命运
天然幂等的。查询接口,调一百次结果都一样。删指定 ID 的资源,第二次调用返回「不存在」但状态一致。这类放心重试,基本不用想。
天然不幂等的。扣款、插入带自增主键的记录、发一条消息、计数器加一。这类每调一次就产生一次真实效果,重试前必须先解决「怎么防止重复生效」。
看起来幂等其实不幂等的。最阴险的一类。比如「把状态更新为已支付」看似幂等,但如果状态流转上还挂了副作用——发通知、加积分、触发回调——那么两次成功调用就会触发两轮副作用。表面状态一致,背后账全乱了。
让不幂等的操作变幂等
业界成熟的解法其实就一句话:让重复请求可以被识别,然后被丢弃。
幂等键(Idempotency Key)。调用方为每个业务操作生成唯一 ID,随请求带上。服务端维护一张键表:第一次见到就执行并记录,第二次见到直接返回上次的结果。支付系统普遍用这套, stripe 的 API 设计是教科书式的范例。
业务唯一约束。用业务本身的天然唯一性做防线:订单号唯一索引、「同一用户同一商品同一秒只允许一单」的唯一键。重复请求进来,数据库层直接拒绝,最硬也最可靠。
条件更新。把「更新状态」改成带前置条件的更新:UPDATE orders SET status='paid' WHERE id=? AND status='unpaid'。第二次执行影响行数为零,副作用代码自然就不会跑。这是我最偏好的方案——不需要额外存储,语义还精确。
配套的两个习惯
重试必须有边界。次数上限、指数退避、超时熔断,三样缺一不可。没有退避的重试在故障期间等于 DDoS 自己:服务越慢,重试越密,越重试越慢。
重试范围要收窄。不要把整个流程包在一个大重试里。调用十个下游的重试策略应该各自独立——查询类可以激进重试,写入类必须先确认幂等再重试。一把梭的统一重试策略,等于把最脆弱的那个下游的风险平摊给了所有人。
写在最后
重试和幂等的关系,本质上是「失败恢复」和「重复防护」的关系。前者是乐观的,相信第二次会成功;后者是悲观的,防着第二次本来就是第一次的回声。
下一次往代码里加重试之前,先停下来问一句:这个操作被调用两次,会发生什么?答案如果是「再扣一次钱」,那你要写的第一行代码不是重试,而是幂等。
- 点赞
- 收藏
- 关注作者
评论(0)