⭐ 推荐:社区规则条款 V1.0

PHP 接口幂等性设计:防重提交的 4 种实现方案

zero
zero 见习用户见习用户
发布于 2026-09-23 19:55 ·92 浏览 ·0 回复
内容摘要

PHP 接口防重提交的幂等性设计需先以一次业务请求的唯一标识判定重复,即由前端在每次渲染表单或点击时生成 request_id 并放入隐藏字段或请求头,再根据适用场景与潜在问题选择四种实现方案,使重复点击只生效一次。

学完这篇,你能给自己的 PHP 接口加上一套「重复提交怎么点都只生效一次」的防护,知道四种方案各自的适用场景和坑在哪。

第一步:先定义清楚"什么算重复"

幂等的判定维度通常不是"IP"或"内容相同",而是一次业务请求的唯一标识。做法很简单:前端每次渲染表单/每次点击生成一个 request_id(UUID 或时间戳+随机串),放进隐藏字段或请求头:

<input type="hidden" name="request_id" value="<?= uuid() ?>">

后端所有方案都围绕这个 request_id 或业务主键展开。注意:同一个 request_id 重复到达 = 重复请求,第一次的返回值要缓存下来返还给后面几次。

第二步:方案一 —— 数据库唯一索引(兜底必做)

成本最低、最可靠的一层。建表时把去重维度做成唯一键:

ALTER TABLE `orders` ADD UNIQUE KEY `uk_user_req` (`user_id`,`request_id`);

插入时直接捕获重复键错误码,不写额外查询:

try {
    $pdo->prepare("INSERT INTO orders(user_id, request_id, amount) VALUES(?,?,?)")
        ->execute([$uid, $requestId, $amount]);
} catch (PDOException $e) {
    if ($e->errorInfo[1] === 1062) {   // MySQL 重复键
        return $this->returnOk($requestId); // 返回首次结果
    }
    throw $e;
}

注意:request_id 要来自客户端而非服务端自己生成,否则每次请求都是"新请求",唯一索引形同虚设。

第三步:方案二 —— Token 令牌(挡误触最有效)

思路是"令牌一次性消费"。表单渲染时生成 token 存入 Redis,TTL 给 600 秒:

$redis->setex("idem:token:$token", 600, 1);

提交时用 DEL 原子消费,返回 1 才放行:

if (!$redis->del("idem:token:$token")) {
    exit('请勿重复提交');
}

DEL 本身是原子操作,天然抗并发,不需要先 GET 再 DEL。

注意:CSRF token 不等于幂等 token。CSRF token 只防跨站伪造,同一个值可以反复提交成功;要做幂等必须用"消费即失效"的一次性 token。系统里那套"提交失败自动拉新 token 重发一次"的机制属于 CSRF 重试,别把它当成防重手段。

第四步:方案三 —— 状态机 + 乐观锁(业务型接口首选)

订单、悬赏采纳这类有明确状态流转的场景,把幂等判断压进 SQL 的 WHERE:

$stmt = $pdo->prepare(
    "UPDATE orders SET status='paid', version=version+1
     WHERE id=? AND status='unpaid' AND version=?"
);
$stmt->execute([$orderId, $version]);

if ($stmt->rowCount() === 0) {
    return $this->returnOk($orderId); // 已被处理过,直接返回
}

一次 SQL 同时完成了"校验+更新",不需要加锁也不会重复扣款。同理,悬赏采纳可以写成 WHERE id=? AND status='open' AND user_id!=?,天然挡住重复采纳和自己的回复。

第五步:方案四 —— Redis 分布式锁(管并发,不管重复)

$lockKey = "lock:pay:{$orderId}";
$ok = $redis->set($lockKey, 1, ['nx', 'ex' => 10]);
if (!$ok) {
    exit('正在处理中,请稍候');
}
try {
    // 业务逻辑
} finally {
    $redis->del($lockKey);
}

参数要点:nx 保证只有一个请求抢到,ex 是自动过期时间,必须大于业务最长耗时。

注意:分布式锁只解决"同一瞬间多个请求并行进入",锁一过期第二次请求照样能进来。所以它只能作为辅助层,必须配合方案一或方案三使用。

小结

  • 定标识:客户端生成 request_id,一切幂等判断围绕它。
  • 唯一索引兜底:user_id + request_id 建唯一键,捕获 1062 错误返回首次结果,这层不能省。
  • 一次性 Token:Redis DEL 原子消费,专门挡用户手抖连点。
  • 状态机 + 乐观锁:UPDATE ... WHERE status='旧状态',用 rowCount() 判断是否已处理。
  • 分布式锁:只防并发,不防重复,ex 到期时间要留够余量。
  • 前端按钮置灰、禁用只是体验优化,绝不能当作防重手段。
  • 实际落地按「Token → 状态机 → 唯一索引」三层叠加,任何一层漏掉都还有下一层接着。
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-573.html
转载请注明出处,版权归原作者所有。
他们都看过 1 人浏览过
CLARA轻量论坛系统
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 0

还没有回复,来抢沙发~