本文目录导读:

在 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 小时)
- 原子操作防止竞态条件
- 错误处理(超时、服务重启等)
- 日志记录以便问题排查
选择哪种方案取决于你的业务场景、并发量、数据一致性要求等因素,对于金融类接口,建议使用数据库事务 + 唯一约束 + 幂等性令牌的组合方案。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播