本文目录导读:

在PHP中保证接口幂等性,核心思想是确保同一个请求执行多次,其结果与执行一次完全相同,以下是完整的解决方案体系:
什么是幂等性
// 幂等操作示例 GET /api/user/1 // 幂等:多次查询结果一致 PUT /api/user/1 // 幂等:更新为相同值,结果一致 DELETE /api/user/1 // 幂等:删除后再次删除,返回相同结果 // 非幂等操作示例 POST /api/payment // 非幂等:每次提交都会扣款 POST /api/order/create // 非幂等:每次创建新订单
核心实现方案
Token机制(最常用)
<?php
class IdempotencyToken {
private $redis;
public function __construct($redis) {
$this->redis = $redis;
}
/**
* 生成幂等性Token
*/
public function generateToken() {
$token = bin2hex(random_bytes(16));
// 存储token,设置有效期(如24小时)
$this->redis->setex("idem:token:" . $token, 86400, 'pending');
return $token;
}
/**
* 检查并锁定Token(原子操作)
*/
public function checkAndLock($token) {
// 使用Lua脚本保证原子性
$script = <<<LUA
local token = KEYS[1]
local status = redis.call('get', token)
if status == false then
return 'invalid' // 不存在的token
elseif status == 'pending' then
redis.call('set', token, 'processing')
return 'success' // 首次处理
elseif status == 'processing' then
return 'processing' // 处理中(重复请求)
elseif status == 'completed' then
return 'completed' // 已完成
end
LUA;
$result = $this->redis->eval($script, [$token], 1);
return $result;
}
/**
* 标记完成
*/
public function markCompleted($token) {
$this->redis->set("idem:token:" . $token, 'completed');
$this->redis->expire("idem:token:" . $token, 86400);
}
}
// 使用示例
class PaymentController {
public function createOrder(Request $request) {
$token = $request->header('Idempotency-Token');
if (empty($token)) {
return response()->json(['error' => '缺少幂等性token'], 400);
}
$idem = new IdempotencyToken($this->redis);
$result = $idem->checkAndLock($token);
switch ($result) {
case 'success':
// 执行业务逻辑
try {
$order = $this->createOrderLogic($request);
$idem->markCompleted($token);
return response()->json($order);
} catch (\Exception $e) {
// 处理失败,重置token状态
$this->redis->set("idem:token:" . $token, 'pending');
throw $e;
}
case 'processing':
// 重复请求,等待处理
return response()->json(['message' => '请求处理中'], 202);
case 'completed':
// 返回缓存的响应
return response()->json($this->getCachedResponse($token));
case 'invalid':
return response()->json(['error' => '无效的token'], 400);
}
}
}
数据库唯一约束
<?php
class OrderService {
private $db;
/**
* 创建订单(使用唯一键约束)
*/
public function createOrder($userId, $productId, $requestId) {
try {
$sql = "INSERT INTO orders (user_id, product_id, request_id, status)
VALUES (?, ?, ?, 'processing')";
$this->db->execute($sql, [$userId, $productId, $requestId]);
return ['success' => true, 'order_id' => $this->db->lastInsertId()];
} catch (PDOException $e) {
// 检查是否是唯一键冲突
if ($e->getCode() == 23000) { // SQLSTATE[23000]: Integrity constraint violation
// 查询已存在的订单
$sql = "SELECT * FROM orders WHERE request_id = ? LIMIT 1";
$existingOrder = $this->db->query($sql, [$requestId]);
if ($existingOrder) {
return ['success' => true, 'order_id' => $existingOrder['id'], 'duplicate' => true];
}
}
throw $e;
}
}
}
// 数据库表设计
/*
CREATE TABLE orders (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
product_id INT NOT NULL,
request_id VARCHAR(32) UNIQUE NOT NULL, // 唯一约束
status VARCHAR(20) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uniq_request_id (request_id)
);
*/
Redis SetNX 原子操作
<?php
class IdempotencyHandler {
private $redis;
public function __construct($redis) {
$this->redis = $redis;
}
/**
* 处理幂等请求
*/
public function process($idempotencyKey, callable $callback) {
$lockKey = "idem:lock:" . $idempotencyKey;
$resultKey = "idem:result:" . $idempotencyKey;
// 尝试获取锁(5分钟过期)
$acquired = $this->redis->set($lockKey, '1', ['NX', 'EX' => 300]);
if (!$acquired) {
// 检查是否有已完成的结果
$cachedResult = $this->redis->get($resultKey);
if ($cachedResult !== false) {
return json_decode($cachedResult, true);
}
// 没有完成结果,可能是处理中
throw new \Exception('Request is being processed');
}
try {
// 执行业务逻辑
$result = $callback();
// 缓存结果(10分钟)
$this->redis->setex($resultKey, 600, json_encode($result));
// 释放锁
$this->redis->del($lockKey);
return $result;
} catch (\Exception $e) {
// 释放锁
$this->redis->del($lockKey);
throw $e;
}
}
}
高级实践方案
请求指纹 + 缓存
<?php
class RequestFingerprint {
/**
* 生成请求指纹
*/
public static function fingerprint(Request $request) {
// 获取业务相关参数
$params = $request->all();
ksort($params); // 排序保证一致性
// 生成MD5
$json = json_encode($params);
return md5($json);
}
}
// 中间件实现
class IdempotencyMiddleware {
public function handle($request, \Closure $next) {
// 只处理POST、PUT、PATCH、DELETE请求
if (!in_array($request->method(), ['POST', 'PUT', 'PATCH', 'DELETE'])) {
return $next($request);
}
// 组合幂等键
$fingerprint = RequestFingerprint::fingerprint($request);
$idemKey = "idem:" . md5($request->path() . ':' . $fingerprint);
// 检查Redis
$cached = app('redis')->get($idemKey);
if ($cached) {
return response()->json(json_decode($cached, true));
}
// 添加唯一请求ID
$requestId = $request->header('X-Request-ID') ?: uniqid('', true);
// 尝试原子锁定
$locked = app('redis')->set("lock:{$idemKey}", $requestId, ['NX', 'EX' => 60]);
if (!$locked) {
// 处理中是重复请求
return response()->json(['error' => '请求处理中'], 202);
}
// 执行业务
$response = $next($request);
// 缓存响应
if ($response->getStatusCode() == 200) {
app('redis')->setex($idemKey, 600, $response->getContent());
app('redis')->del("lock:{$idemKey}");
}
return $response;
}
}
数据库乐观锁
<?php
class OptimisticLockService {
/**
* 更新库存(乐观锁)
*/
public function deductInventory($productId, $quantity, $version) {
$sql = "UPDATE products
SET stock = stock - ?,
version = version + 1
WHERE id = ? AND version = ? AND stock >= ?";
$affected = $this->db->execute($sql, [
$quantity,
$productId,
$version,
$quantity
]);
if ($affected === 0) {
// 版本不匹配或库存不足(重复请求或并发冲突)
throw new \Exception('操作失败,请重试');
}
return true;
}
}
完整实现示例
<?php
namespace App\Services;
class IdempotencyService {
private $redis;
private $db;
private $cache;
public function __construct() {
$this->redis = app('redis');
$this->db = app('db');
$this->cache = app('cache');
}
/**
* 统一幂等处理入口
*/
public function execute($idempotencyKey, callable $businessLogic) {
// 1. 检查是否已完成
$resultKey = "idem:result:{$idempotencyKey}";
$cachedResult = $this->cache->get($resultKey);
if ($cachedResult !== null) {
return [
'data' => unserialize($cachedResult),
'from_cache' => true
];
}
// 2. 尝试获取处理锁
$lockKey = "idem:lock:{$idempotencyKey}";
$lockToken = uniqid('', true);
$locked = $this->redis->set($lockKey, $lockToken, ['NX', 'EX' => 60]);
if (!$locked) {
// 3. 被锁定,等待或返回处理中
throw new IdempotencyException('请求已提交,正在处理中', 202);
}
try {
// 4. 执行业务逻辑
$result = $businessLogic();
// 5. 缓存结果
$this->cache->put($resultKey, serialize($result), 600);
// 6. 释放锁
$this->releaseLock($lockKey, $lockToken);
return [
'data' => $result,
'from_cache' => false
];
} catch (\Exception $e) {
// 7. 失败时释放锁
$this->releaseLock($lockKey, $lockToken);
throw $e;
}
}
/**
* 安全释放锁
*/
private function releaseLock($lockKey, $lockToken) {
$script = <<<LUA
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
LUA;
$this->redis->eval($script, [$lockKey, $lockToken], 1);
}
/**
* 查询幂等结果
*/
public function getResult($idempotencyKey) {
$resultKey = "idem:result:{$idempotencyKey}";
$cachedResult = $this->cache->get($resultKey);
if ($cachedResult !== null) {
return unserialize($cachedResult);
}
return null;
}
}
/**
* 控制器使用示例
*/
class PaymentController extends Controller {
public function submitPayment(Request $request) {
// 获取或生成幂等键
$idempotencyKey = $request->header('Idempotency-Key')
?: $request->input('order_no')
?: uniqid('pay_', true);
$service = new IdempotencyService();
try {
$result = $service->execute($idempotencyKey, function () use ($request) {
// 业务逻辑
return $this->processPayment($request->all());
});
return response()->json([
'code' => 200,
'data' => $result['data'],
'cache' => $result['from_cache']
]);
} catch (IdempotencyException $e) {
return response()->json(['code' => 202, 'message' => $e->getMessage()], 202);
}
}
}
幂等性方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Token机制 | 控制灵活、适用于所有请求 | 需要前后端配合、Token管理 | 支付、下单等关键操作 |
| 数据库唯一约束 | 简单可靠、数据库本身保证 | 增加表字段、性能略低 | 订单创建、注册 |
| Redis SetNX | 高性能、原子性 | Redis故障需处理、结果暂存 | 高并发接口 |
| 请求指纹 | 无需客户端配合、自动实现 | 参数变化会导致误判 | 通用API接口 |
| 乐观锁 | 并发控制好、无锁阻塞 | 需额外版本字段、冲突需重试 | 库存扣减、状态更新 |
最佳实践建议
// 1. 综合方案(推荐)
class BestPracticesIdempotency {
public static function execute(Request $request, callable $callback) {
// 获取幂等键
$idemKey = $request->header('Idempotency-Key');
if (!$idemKey) {
// 自动生成
$idemKey = RequestFingerprint::fingerprint($request);
}
// 多级缓存策略
$cacheKey = "idem:{$idemKey}";
// 1. 内存缓存(最快)
if (Cache::has($cacheKey)) {
return Cache::get($cacheKey);
}
// 2. Redis分布式锁
$lock = Redis::set("lock:{$idemKey}", '1', ['NX', 'EX' => 60]);
if (!$lock) {
// 3. 等待重试
for ($i = 0; $i < 5; $i++) {
if (Cache::has($cacheKey)) {
return Cache::get($cacheKey);
}
usleep(100000); // 100ms
}
throw new \Exception('请求处理中,请稍后');
}
try {
// 执行核心业务
$result = $callback();
// 缓存结果(使用Redis)
Redis::setex($cacheKey, 600, json_encode($result));
// 释放锁
Redis::del("lock:{$idemKey}");
return $result;
} catch (\Exception $e) {
Redis::del("lock:{$idemKey}");
throw $e;
}
}
}
测试验证
<?php
class IdempotencyTest extends PHPUnit\Framework\TestCase {
public function testIdempotency() {
$client = new GuzzleHttp\Client();
// 生成幂等键
$idemKey = uniqid('test_', true);
// 第一次请求
$response1 = $client->request('POST', '/api/order/create', [
'headers' => ['Idempotency-Key' => $idemKey],
'json' => ['product_id' => 1, 'quantity' => 2]
]);
// 第二次请求(相同键)
$response2 = $client->request('POST', '/api/order/create', [
'headers' => ['Idempotency-Key' => $idemKey],
'json' => ['product_id' => 1, 'quantity' => 2]
]);
$data1 = json_decode($response1->getBody(), true);
$data2 = json_decode($response2->getBody(), true);
$this->assertEquals($data1['order_id'], $data2['order_id']);
$this->assertEquals(200, $response1->getStatusCode());
$this->assertEquals(200, $response2->getStatusCode());
}
}
通过以上方案的实施,能够有效保证PHP接口的幂等性,防止重复提交带来的业务风险和数据错误。