PHP 接口幂等不能只靠一张表:从重试语义到分布式一致性
很多人做接口幂等,第一反应是“建一张幂等表,拿请求号去重”。这当然是必要动作,但远远不够。因为真正的问题不在“有没有重复请求”,而在“系统如何定义同一次业务意图,以及这个意图在多步执行中如何保持一致”。
先定义什么叫“同一次请求”
客户端重试、网关超时重放、消息队列重复投递,本质上都不是同一个技术问题,但业务上常常要被视为同一次意图。幂等键必须绑定业务语义,而不是绑定 transport 层细节。比如支付创建,幂等键更适合由商户单号和动作类型推导,而不是简单拿 HTTP request id 顶上。
幂等表只解决“看见过”,不解决“执行到哪”
真正麻烦的是中间态:数据库已写,MQ 未发;外部接口已调用,事务未提交;状态已推进,但响应还没返回。此时即使幂等表记录了“处理过”,也未必知道该不该重试、重试到哪一步、是否要补偿。因此幂等要和业务状态机一起设计,而不是孤立成一个拦截器。
把“结果缓存”与“执行锁”区分开
很多实现把 Redis setnx、数据库唯一键、响应缓存混成一层。更清晰的拆法是:执行锁负责限制并发进入,结果缓存负责对重复请求返回同一业务结果,状态机负责判断中途失败后的恢复路径。三者职责不同,不能互相代替。
// 伪代码:先拿幂等记录,再根据状态推进
if ($record = $repo->findByKey($idempotencyKey)) {
return $record->toResponse();
}
$lock->acquire($idempotencyKey);
try {
$state = $service->execute($command);
$repo->storeResult($idempotencyKey, $state);
} finally {
$lock->release($idempotencyKey);
}结论
幂等从来不是“防重”这么简单。它真正要回答的是:当世界不可靠、调用会重试、消息会重复时,系统能否把同一业务意图收敛到一个稳定结果。只靠一张表,通常不够。