本文目录导读:

在 PHP API 中保证幂等性,核心思路是利用唯一标识符(Idempotency Key)来识别并屏蔽重复请求,以下是几种主流且可靠的实现方案:
幂等性令牌(Idempotency Key)方案(推荐)
原理
- 客户端在请求头中携带一个全局唯一的
Idempotency-Key - 服务端记录
Key与处理结果,相同Key的重复请求直接返回已存储的结果
实现示例
<?php
class IdempotentMiddleware {
private $redis;
private $ttl = 86400; // 24小时过期
public function handle($request, $next) {
$idempotencyKey = $request->header('Idempotency-Key');
if (!$idempotencyKey) {
return response()->json(['error' => '缺少幂等性令牌'], 400);
}
// 检查是否已处理过
$cachedResult = $this->redis->get("idempotent:{$idempotencyKey}");
if ($cachedResult !== null) {
// 重复请求,返回缓存结果
return response()->json(json_decode($cachedResult, true));
}
// 执行实际业务逻辑
$result = $next($request);
// 存储结果
$this->redis->setex(
"idempotent:{$idempotencyKey}",
$this->ttl,
json_encode($result->getData())
);
return $result;
}
}
客户端使用
POST /api/payment
Idempotency-Key: uuid-xxxx-xxxx-xxxx
Content-Type: application/json
{
"amount": 100,
"currency": "CNY"
}
数据库唯一约束方案
原理
- 在数据库层面设置唯一索引来防止重复插入
- 适用于新增数据的幂等性保障
<?php
class OrderService {
public function createOrder($data) {
try {
// 使用业务唯一标识作为幂等性保证
$order = Order::create([
'order_no' => $data['order_no'], // 业务唯一编号
'product_id' => $data['product_id'],
'amount' => $data['amount']
]);
return ['success' => true, 'data' => $order];
} catch (\Illuminate\Database\QueryException $e) {
// 违反唯一约束,说明重复请求
if ($e->getCode() == '23000') {
return ['success' => true, 'message' => '订单已存在'];
}
throw $e;
}
}
}
数据库表设计
CREATE TABLE orders (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
order_no VARCHAR(64) NOT NULL UNIQUE, -- 唯一约束
product_id INT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_order_no (order_no)
);
Redis + Lua 脚本(原子操作)
原理
- 使用 Lua 脚本保证检查和处理是原子操作
- 避免并发请求下的竞态条件
class RedisIdempotent {
private $redis;
public function processWithIdempotent($key, callable $callback) {
$luaScript = <<<'LUA'
local key = KEYS[1]
local ttl = ARGV[1]
-- 设置一个临时的锁,防止并发
local lock = redis.call('SET', key, 'PROCESSING', 'NX', 'EX', ttl)
if not lock then
local status = redis.call('GET', key)
if status == 'PROCESSING' then
return 'PROCESSING'
end
return status
end
return 'PROCESSING'
LUA;
$result = $this->redis->eval($luaScript, 1, $key, 30);
if ($result === 'PROCESSING') {
throw new \Exception('请求正在处理中');
}
if ($result !== null) {
return json_decode($result, true);
}
// 执行业务逻辑
$data = $callback();
// 存储结果
$this->redis->setex($key, 86400, json_encode($data));
return $data;
}
}
分布式锁方案
原理
- 使用 RedLock 等分布式锁防止并发重复操作
- 适合对一致性要求极高的场景
use Illuminate\Support\Facades\Redis;
class PaymentService {
public function processPayment($paymentId, $amount) {
$lockKey = "payment:lock:{$paymentId}";
$lock = Redis::lock($lockKey, 10); // 10秒超时
try {
if (!$lock->get()) {
throw new \Exception('支付正在处理中');
}
// 检查是否已支付
$payment = Payment::find($paymentId);
if ($payment->status === 'paid') {
return ['code' => 200, 'message' => '已支付'];
}
// 执行实际支付逻辑
$payment->status = 'paid';
$payment->save();
return ['code' => 200, 'message' => '支付成功'];
} finally {
$lock->release(); // 释放锁
}
}
}
幂等性最佳实践
策略选择矩阵
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 支付接口 | Idempotency Key + 数据库事务 | 需防止重复扣款 |
| 订单创建 | 数据库唯一约束 | 简单高效 |
| 敏感操作 | Redis + Lua 脚本 | 原子操作保证 |
| 高并发 | 分布式锁 + 幂等性令牌 | 防止并发冲突 |
完整实现示例
class IdempotentService {
private $redis;
private $db;
public function createOrderWithIdempotent($idempotencyKey, $data) {
// 1. 检查幂等性
$existingResult = $this->redis->get("order:{$idempotencyKey}");
if ($existingResult) {
return json_decode($existingResult, true);
}
// 2. 开始数据库事务
$this->db->beginTransaction();
try {
// 3. 使用 SELECT ... FOR UPDATE 防止幻读
$lock = $this->db->select("SELECT * FROM idempotent_keys
WHERE key_value = ? FOR UPDATE",
[$idempotencyKey]);
if (!empty($lock)) {
$this->db->rollBack();
return json_decode($lock[0]['result'], true);
}
// 4. 执行业务逻辑
$order = $this->doCreateOrder($data);
// 5. 记录幂等性键
$this->db->insert("INSERT INTO idempotent_keys
(key_value, result, created_at)
VALUES (?, ?, NOW())",
[$idempotencyKey, json_encode($order)]);
// 6. 提交事务
$this->db->commit();
// 7. 缓存结果到 Redis
$this->redis->setex("order:{$idempotencyKey}", 86400, json_encode($order));
return $order;
} catch (\Exception $e) {
$this->db->rollBack();
throw $e;
}
}
}
幂等性验证中间件(Laravel 示例)
// app/Http/Middleware/IdempotentMiddleware.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Redis;
class IdempotentMiddleware
{
public function handle($request, Closure $next)
{
if (in_array($request->method(), ['GET', 'HEAD', 'OPTIONS'])) {
return $next($request);
}
$key = $request->header('Idempotency-Key');
if (!$key) {
return response()->json([
'error' => '幂等性密钥缺失',
'code' => 'IDEMPOTENCY_KEY_MISSING'
], 400);
}
$result = Redis::get("idempotent:{$key}");
if ($result) {
return response()->json(json_decode($result, true), 200);
}
// 设置防重入标志
$locked = Redis::set("idempotent:{$key}", json_encode([
'status' => 'processing'
]), 'NX', 'EX', 120);
if (!$locked) {
return response()->json([
'error' => '该请求正在处理中',
'code' => 'REQUEST_PROCESSING'
], 409);
}
$response = $next($request);
// 存储最终结果
$content = $response->getContent();
Redis::setex("idempotent:{$key}", 86400, $content);
return $response;
}
}
- 首选方案:Idempotency Key + Redis 缓存 + 数据库事务
- 核心原则:客户端生成唯一 Key,服务端记录并校验
- 重要考虑:
- Key 的过期时间(建议 24 小时)
- 原子操作防止竞态条件
- 错误处理(超时、服务重启等)
- 日志记录以便问题排查
选择哪种方案取决于你的业务场景、并发量、数据一致性要求等因素,对于金融类接口,建议使用数据库事务 + 唯一约束 + 幂等性令牌的组合方案。