PHP接口幂等性如何保证

wen PHP项目 1

本文目录导读:

PHP接口幂等性如何保证

  1. 什么是幂等性
  2. 核心实现方案
  3. 高级实践方案
  4. 完整实现示例
  5. 幂等性方案对比
  6. 最佳实践建议
  7. 测试验证

在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接口的幂等性,防止重复提交带来的业务风险和数据错误。

抱歉,评论功能暂时关闭!