本文目录导读:

在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接口的幂等性,防止重复提交带来的业务风险和数据错误。
版权声明: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直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播