PHP实现幂等性(Idempotency):在API层对POST/PUT请求的重复提交防御

好的,下面是关于PHP实现幂等性,特别是在API层防御POST/PUT请求重复提交的技术文章。

PHP API幂等性实现:防御POST/PUT请求重复提交

大家好,今天我们来探讨一个非常重要的API设计原则:幂等性。特别是在处理POST和PUT请求时,如何防御重复提交,保证数据的一致性。幂等性是指一个操作,无论执行多少次,其结果都相同。对于API来说,这意味着多次调用同一个接口,产生的影响应该与调用一次相同。

为什么需要幂等性?

在高并发、网络不稳定的环境下,客户端可能会因为超时、网络抖动等原因多次发送相同的请求。如果没有幂等性保证,这些重复请求可能会导致数据错误,例如:

  • 重复创建订单
  • 重复增加库存
  • 重复扣款

幂等性实现策略

实现幂等性的方法有很多,针对不同的场景需要选择合适的策略。以下是一些常用的策略,以及如何在PHP API中实现它们:

  1. Token机制(推荐)
  2. 唯一性约束
  3. 乐观锁
  4. 悲观锁
  5. 状态机
  6. 记录请求日志

接下来,我们将逐一详细介绍这些策略,并提供PHP代码示例。

1. Token机制

Token机制是最常用且推荐的幂等性实现方式。客户端在发起请求前,先从服务器获取一个唯一的Token,然后在请求体中携带这个Token。服务器端接收到请求后,首先验证Token是否有效,然后执行业务逻辑。如果Token有效,则执行操作,并将Token标记为已使用。如果Token无效,则拒绝请求。

  • 流程:

    1. 客户端向服务器请求Token。
    2. 服务器生成唯一Token,并保存(例如Redis),设置过期时间。
    3. 客户端携带Token发起POST/PUT请求。
    4. 服务器验证Token:
      • Token存在且未被使用:执行业务逻辑,将Token标记为已使用(或删除),返回成功。
      • Token不存在或已被使用:拒绝请求,返回错误。
  • PHP代码示例:

    <?php
    
    class IdempotencyService
    {
        private $redis;
        private $tokenPrefix = 'idempotency_token:';
        private $tokenTTL = 60; // Token有效期,单位秒
    
        public function __construct()
        {
            // 假设你已经配置好了Redis连接
            $this->redis = new Redis();
            $this->redis->connect('127.0.0.1', 6379);
        }
    
        /**
         * 生成Token
         * @return string
         */
        public function generateToken(): string
        {
            $token = uniqid(); //或者使用uuid
            $key = $this->tokenPrefix . $token;
            $this->redis->setex($key, $this->tokenTTL, 1); // 1代表存在
            return $token;
        }
    
        /**
         * 验证Token
         * @param string $token
         * @return bool
         */
        public function validateToken(string $token): bool
        {
            $key = $this->tokenPrefix . $token;
            if ($this->redis->exists($key)) {
                // 使用Redis的原子操作 del 来保证并发安全
                if ($this->redis->del($key)) { //如果能删除,说明token存在且未被使用
                    return true;
                } else {
                    // 并发情况下可能删除失败,说明已被使用
                    return false;
                }
            }
            return false;
        }
    
        /**
         * 示例API接口
         * @param array $data
         * @return array
         */
        public function processRequest(array $data): array
        {
            if (!isset($data['token'])) {
                return ['status' => 'error', 'message' => 'Token is required'];
            }
    
            $token = $data['token'];
    
            if (!$this->validateToken($token)) {
                return ['status' => 'error', 'message' => 'Invalid or expired token'];
            }
    
            // 模拟业务逻辑处理
            // ... 你的业务逻辑代码 ...
            $result = $this->performBusinessLogic($data);
    
            return ['status' => 'success', 'message' => 'Request processed successfully', 'data' => $result];
        }
    
        private function performBusinessLogic(array $data): array
        {
            // 模拟业务逻辑
            // 假设这里是将数据保存到数据库
            // 为了简单起见,我们直接返回数据
            return $data;
        }
    }
    
    // 使用示例
    $idempotencyService = new IdempotencyService();
    
    // 1. 客户端首先获取Token
    $token = $idempotencyService->generateToken();
    echo "Generated Token: " . $token . "n";
    
    // 2. 客户端携带Token发起请求
    $requestData = [
        'token' => $token,
        'name' => 'Product A',
        'price' => 100,
    ];
    
    // 3. 服务器处理请求
    $response = $idempotencyService->processRequest($requestData);
    print_r($response);
    
    // 4. 模拟重复请求
    echo "nSimulating a duplicate request...n";
    $response2 = $idempotencyService->processRequest($requestData);
    print_r($response2);
    
    ?>

    代码解释:

    • generateToken(): 生成唯一的Token,并将其存储在Redis中,设置过期时间。
    • validateToken(): 验证Token是否存在,如果存在则删除,并返回true;否则返回false。 这里使用了redis的del命令,它本身是原子性的,避免了并发问题。
    • processRequest(): 接收请求,验证Token,然后执行业务逻辑。
    • 注意:实际生产环境中,需要替换performBusinessLogic()中的模拟业务逻辑为真正的业务逻辑代码。

    优点:

    • 简单易实现。
    • 适用于各种场景,包括创建、更新等操作。
    • 可以设置Token的过期时间,防止Token无限期占用资源。

    缺点:

    • 需要额外的存储空间来存储Token(例如Redis)。
    • 客户端需要先请求Token,增加了一次网络请求。
  1. 唯一性约束

如果你的业务场景涉及到数据库操作,并且数据库中有唯一性约束的字段,那么可以利用唯一性约束来实现幂等性。例如,订单号是唯一的,那么在创建订单时,如果订单号已经存在,则直接返回成功,而不是重复创建订单。

  • 流程:

    1. 客户端发起POST/PUT请求,携带唯一标识符(例如订单号)。
    2. 服务器端接收到请求后,尝试将数据插入数据库。
    3. 如果插入成功,则返回成功。
    4. 如果插入失败,并且错误原因是唯一性约束冲突,则返回成功(表示已经执行过相同的操作)。
    5. 如果插入失败,并且错误原因不是唯一性约束冲突,则返回错误。
  • PHP代码示例:

    <?php
    
    class OrderService
    {
        private $pdo;
    
        public function __construct(PDO $pdo)
        {
            $this->pdo = $pdo;
        }
    
        /**
         * 创建订单
         * @param string $orderId 订单ID,作为唯一标识
         * @param int $userId 用户ID
         * @param float $amount 订单金额
         * @return array
         */
        public function createOrder(string $orderId, int $userId, float $amount): array
        {
            try {
                $sql = "INSERT INTO orders (order_id, user_id, amount, created_at) VALUES (:order_id, :user_id, :amount, NOW())";
                $stmt = $this->pdo->prepare($sql);
                $stmt->execute([
                    'order_id' => $orderId,
                    'user_id' => $userId,
                    'amount' => $amount,
                ]);
    
                return ['status' => 'success', 'message' => 'Order created successfully'];
    
            } catch (PDOException $e) {
                // 检查是否是唯一性约束冲突
                if ($e->getCode() == '23000' && strpos($e->getMessage(), 'order_id') !== false) {
                    // 唯一性约束冲突,表示订单已经存在,返回成功
                    return ['status' => 'success', 'message' => 'Order already exists'];
                } else {
                    // 其他错误,返回错误信息
                    return ['status' => 'error', 'message' => 'Failed to create order: ' . $e->getMessage()];
                }
            }
        }
    }
    
    // 使用示例
    try {
        // 假设你已经配置好了PDO连接
        $pdo = new PDO("mysql:host=localhost;dbname=test", "username", "password");
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    
        $orderService = new OrderService($pdo);
    
        // 1. 第一次创建订单
        $orderId = uniqid('order_'); // 生成唯一订单ID
        $response = $orderService->createOrder($orderId, 123, 100.00);
        print_r($response);
    
        // 2. 模拟重复请求,使用相同的订单ID
        echo "nSimulating a duplicate request...n";
        $response2 = $orderService->createOrder($orderId, 123, 100.00);
        print_r($response2);
    
    } catch (PDOException $e) {
        echo "Database connection failed: " . $e->getMessage();
    }
    ?>

    数据库表结构:

    CREATE TABLE orders (
        id INT AUTO_INCREMENT PRIMARY KEY,
        order_id VARCHAR(255) UNIQUE NOT NULL,  -- 唯一索引
        user_id INT NOT NULL,
        amount DECIMAL(10, 2) NOT NULL,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );

    代码解释:

    • createOrder(): 尝试将订单数据插入数据库。
    • 捕获PDOException异常,判断是否是唯一性约束冲突(错误代码为23000,并且错误信息包含order_id)。
    • 如果是唯一性约束冲突,则返回成功;否则返回错误。
    • 注意:你需要根据你使用的数据库类型,修改唯一性约束冲突的判断逻辑。

    优点:

    • 实现简单,不需要额外的存储空间。
    • 性能较好,直接利用数据库的索引。

    缺点:

    • 只适用于创建操作,不适用于更新操作。
    • 需要数据库支持唯一性约束。
    • 错误判断逻辑可能依赖于具体的数据库实现。
  1. 乐观锁

乐观锁是一种并发控制机制,它假设在操作期间不会发生冲突。在更新数据时,先读取数据的版本号,然后在更新时,比较版本号是否一致。如果版本号一致,则更新成功;否则更新失败,表示数据已被其他线程修改。

  • 流程:

    1. 客户端发起PUT请求,携带要更新的数据和版本号。
    2. 服务器端接收到请求后,先读取数据库中对应数据的版本号。
    3. 比较客户端传递的版本号和数据库中的版本号是否一致。
    4. 如果一致,则更新数据,并将版本号加1。
    5. 如果不一致,则拒绝请求,返回错误。
  • PHP代码示例:

    <?php
    
    class ProductService
    {
        private $pdo;
    
        public function __construct(PDO $pdo)
        {
            $this->pdo = $pdo;
        }
    
        /**
         * 更新产品信息
         * @param int $productId 产品ID
         * @param string $name 产品名称
         * @param float $price 产品价格
         * @param int $version 版本号
         * @return array
         */
        public function updateProduct(int $productId, string $name, float $price, int $version): array
        {
            try {
                $sql = "UPDATE products SET name = :name, price = :price, version = version + 1 WHERE id = :id AND version = :version";
                $stmt = $this->pdo->prepare($sql);
                $stmt->execute([
                    'id' => $productId,
                    'name' => $name,
                    'price' => $price,
                    'version' => $version,
                ]);
    
                // 检查更新是否成功
                if ($stmt->rowCount() > 0) {
                    return ['status' => 'success', 'message' => 'Product updated successfully'];
                } else {
                    return ['status' => 'error', 'message' => 'Product update failed: version mismatch'];
                }
    
            } catch (PDOException $e) {
                return ['status' => 'error', 'message' => 'Failed to update product: ' . $e->getMessage()];
            }
        }
    
        /**
         * 获取产品信息(包含版本号)
         * @param int $productId
         * @return array|null
         */
        public function getProduct(int $productId): ?array
        {
            $sql = "SELECT id, name, price, version FROM products WHERE id = :id";
            $stmt = $this->pdo->prepare($sql);
            $stmt->execute(['id' => $productId]);
            $product = $stmt->fetch(PDO::FETCH_ASSOC);
            return $product ?: null;
        }
    }
    
    // 使用示例
    try {
        // 假设你已经配置好了PDO连接
        $pdo = new PDO("mysql:host=localhost;dbname=test", "username", "password");
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    
        $productService = new ProductService($pdo);
    
        // 1. 获取产品信息
        $productId = 1; // 假设产品ID为1
        $product = $productService->getProduct($productId);
    
        if (!$product) {
            echo "Product not foundn";
            exit;
        }
    
        $version = $product['version']; // 获取当前版本号
        echo "Initial product version: " . $version . "n";
    
        // 2. 第一次更新产品信息
        $response = $productService->updateProduct($productId, 'Product A Updated', 120.00, $version);
        print_r($response);
    
        // 3. 模拟重复请求,使用相同的版本号
        echo "nSimulating a duplicate request...n";
        $response2 = $productService->updateProduct($productId, 'Product A Updated Again', 130.00, $version);
        print_r($response2);
    
        //4. 再次获取产品信息,查看版本号是否更新
        $product = $productService->getProduct($productId);
        echo "New product version: " . $product['version'] . "n";
    
    } catch (PDOException $e) {
        echo "Database connection failed: " . $e->getMessage();
    }
    ?>

    数据库表结构:

    CREATE TABLE products (
        id INT AUTO_INCREMENT PRIMARY KEY,
        name VARCHAR(255) NOT NULL,
        price DECIMAL(10, 2) NOT NULL,
        version INT NOT NULL DEFAULT 0  -- 版本号
    );
    
    INSERT INTO products (name, price) VALUES ('Product A', 100.00); //插入测试数据

    代码解释:

    • updateProduct(): 更新产品信息,同时比较版本号是否一致。
    • SQL语句中使用WHERE id = :id AND version = :version来判断版本号是否一致。
    • 如果更新成功,rowCount()会大于0;否则小于等于0,表示版本号不一致。
    • getProduct(): 获取产品信息,包括版本号。

    优点:

    • 并发性能较好,适用于读多写少的场景。
    • 不需要额外的存储空间。

    缺点:

    • 需要额外的版本号字段。
    • 更新失败时,需要客户端重新获取最新数据和版本号,然后重试。
    • 只适用于更新操作。
  1. 悲观锁

悲观锁是一种并发控制机制,它假设在操作期间一定会发生冲突。在操作数据之前,先对数据加锁,防止其他线程修改数据。

  • 流程:

    1. 客户端发起PUT请求,携带要更新的数据。
    2. 服务器端接收到请求后,先对数据库中对应的数据加锁(例如使用SELECT ... FOR UPDATE)。
    3. 更新数据。
    4. 释放锁。
  • PHP代码示例:

    <?php
    
    class ProductService
    {
        private $pdo;
    
        public function __construct(PDO $pdo)
        {
            $this->pdo = $pdo;
        }
    
        /**
         * 更新产品信息 (使用悲观锁)
         * @param int $productId 产品ID
         * @param string $name 产品名称
         * @param float $price 产品价格
         * @return array
         */
        public function updateProduct(int $productId, string $name, float $price): array
        {
            try {
                // 开启事务
                $this->pdo->beginTransaction();
    
                // 1. 获取产品信息并加锁
                $sql = "SELECT id, name, price, version FROM products WHERE id = :id FOR UPDATE";
                $stmt = $this->pdo->prepare($sql);
                $stmt->execute(['id' => $productId]);
                $product = $stmt->fetch(PDO::FETCH_ASSOC);
    
                if (!$product) {
                    // 回滚事务
                    $this->pdo->rollBack();
                    return ['status' => 'error', 'message' => 'Product not found'];
                }
    
                // 2. 更新产品信息
                $sql = "UPDATE products SET name = :name, price = :price WHERE id = :id";
                $stmt = $this->pdo->prepare($sql);
                $stmt->execute([
                    'id' => $productId,
                    'name' => $name,
                    'price' => $price,
                ]);
    
                // 提交事务
                $this->pdo->commit();
    
                return ['status' => 'success', 'message' => 'Product updated successfully'];
    
            } catch (PDOException $e) {
                // 回滚事务
                if ($this->pdo->inTransaction()) {
                    $this->pdo->rollBack();
                }
                return ['status' => 'error', 'message' => 'Failed to update product: ' . $e->getMessage()];
            }
        }
    }
    
    // 使用示例
    try {
        // 假设你已经配置好了PDO连接
        $pdo = new PDO("mysql:host=localhost;dbname=test", "username", "password");
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    
        $productService = new ProductService($pdo);
    
        // 1. 第一次更新产品信息
        $productId = 1; // 假设产品ID为1
        $response = $productService->updateProduct($productId, 'Product A Updated', 120.00);
        print_r($response);
    
        // 2. 模拟重复请求
        echo "nSimulating a duplicate request...n";
        $response2 = $productService->updateProduct($productId, 'Product A Updated Again', 130.00);
        print_r($response2);
    
    } catch (PDOException $e) {
        echo "Database connection failed: " . $e->getMessage();
    }
    ?>

    数据库表结构:

    CREATE TABLE products (
        id INT AUTO_INCREMENT PRIMARY KEY,
        name VARCHAR(255) NOT NULL,
        price DECIMAL(10, 2) NOT NULL,
        version INT NOT NULL DEFAULT 0  -- 版本号
    );
    
    INSERT INTO products (name, price) VALUES ('Product A', 100.00); //插入测试数据

    代码解释:

    • updateProduct(): 更新产品信息,使用悲观锁。
    • beginTransaction(): 开启事务。
    • SELECT ... FOR UPDATE: 对读取的数据加锁,防止其他事务修改。
    • commit(): 提交事务。
    • rollBack(): 回滚事务。

    优点:

    • 可以保证数据的一致性。
    • 实现简单。

    缺点:

    • 并发性能较差,适用于写多读少的场景。
    • 可能导致死锁。
    • 锁的粒度较大,可能会影响其他操作。
  1. 状态机

对于有状态变化的操作,可以使用状态机来实现幂等性。例如,订单状态有:待支付、已支付、已发货、已完成等。在更新订单状态时,只有当订单状态满足条件时,才能更新成功。

  • 流程:

    1. 客户端发起PUT请求,携带要更新的状态。
    2. 服务器端接收到请求后,先读取数据库中当前状态。
    3. 判断当前状态是否允许更新到目标状态。
    4. 如果允许,则更新状态。
    5. 如果不允许,则拒绝请求。
  • PHP代码示例:

    <?php
    
    class OrderService
    {
        private $pdo;
    
        // 定义订单状态常量
        const STATUS_PENDING = 1; // 待支付
        const STATUS_PAID = 2;    // 已支付
        const STATUS_SHIPPED = 3; // 已发货
        const STATUS_COMPLETED = 4; // 已完成
        const STATUS_CANCELLED = 5; // 已取消
    
        public function __construct(PDO $pdo)
        {
            $this->pdo = $pdo;
        }
    
        /**
         * 更新订单状态
         * @param int $orderId 订单ID
         * @param int $newStatus 新的状态
         * @return array
         */
        public function updateOrderStatus(int $orderId, int $newStatus): array
        {
            try {
                // 1. 获取当前订单状态
                $currentStatus = $this->getCurrentOrderStatus($orderId);
    
                if ($currentStatus === null) {
                    return ['status' => 'error', 'message' => 'Order not found'];
                }
    
                // 2. 验证状态转换是否允许
                if (!$this->isStatusTransitionAllowed($currentStatus, $newStatus)) {
                    return ['status' => 'error', 'message' => 'Invalid status transition'];
                }
    
                // 3. 更新订单状态
                $sql = "UPDATE orders SET status = :status WHERE id = :id";
                $stmt = $this->pdo->prepare($sql);
                $stmt->execute([
                    'id' => $orderId,
                    'status' => $newStatus,
                ]);
    
                return ['status' => 'success', 'message' => 'Order status updated successfully'];
    
            } catch (PDOException $e) {
                return ['status' => 'error', 'message' => 'Failed to update order status: ' . $e->getMessage()];
            }
        }
    
        /**
         * 获取当前订单状态
         * @param int $orderId 订单ID
         * @return int|null
         */
        private function getCurrentOrderStatus(int $orderId): ?int
        {
            $sql = "SELECT status FROM orders WHERE id = :id";
            $stmt = $this->pdo->prepare($sql);
            $stmt->execute(['id' => $orderId]);
            $order = $stmt->fetch(PDO::FETCH_ASSOC);
            return $order ? (int)$order['status'] : null;
        }
    
        /**
         * 验证状态转换是否允许
         * @param int $currentStatus 当前状态
         * @param int $newStatus 新的状态
         * @return bool
         */
        private function isStatusTransitionAllowed(int $currentStatus, int $newStatus): bool
        {
            // 定义状态转换规则
            $allowedTransitions = [
                self::STATUS_PENDING => [self::STATUS_PAID, self::STATUS_CANCELLED], // 待支付可以变更为已支付或已取消
                self::STATUS_PAID => [self::STATUS_SHIPPED, self::STATUS_CANCELLED],  // 已支付可以变更为已发货或已取消
                self::STATUS_SHIPPED => [self::STATUS_COMPLETED],                    // 已发货可以变更为已完成
                self::STATUS_COMPLETED => [],                                         // 已完成不能再变更
                self::STATUS_CANCELLED => [],                                         // 已取消不能再变更
            ];
    
            return isset($allowedTransitions[$currentStatus]) && in_array($newStatus, $allowedTransitions[$currentStatus]);
        }
    }
    
    // 使用示例
    try {
        // 假设你已经配置好了PDO连接
        $pdo = new PDO("mysql:host=localhost;dbname=test", "username", "password");
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    
        $orderService = new OrderService($pdo);
    
        // 1. 第一次更新订单状态
        $orderId = 1; // 假设订单ID为1
        $response = $orderService->updateOrderStatus($orderId, OrderService::STATUS_PAID);
        print_r($response);
    
        // 2. 模拟重复请求
        echo "nSimulating a duplicate request...n";
        $response2 = $orderService->updateOrderStatus($orderId, OrderService::STATUS_PAID);
        print_r($response2);
    
        // 3. 尝试非法状态转换
        echo "nAttempting an invalid status transition...n";
        $response3 = $orderService->updateOrderStatus($orderId, OrderService::STATUS_PENDING); // 从已支付变更为待支付
        print_r($response3);
    
    } catch (PDOException $e) {
        echo "Database connection failed: " . $e->getMessage();
    }
    ?>

    数据库表结构:

    CREATE TABLE orders (
        id INT AUTO_INCREMENT PRIMARY KEY,
        status INT NOT NULL DEFAULT 1  -- 订单状态,默认为待支付
    );
    
    INSERT INTO orders (status) VALUES (1); //插入测试数据,状态为待支付

    代码解释:

    • updateOrderStatus(): 更新订单状态。
    • getCurrentOrderStatus(): 获取当前订单状态。
    • isStatusTransitionAllowed(): 验证状态转换是否允许。
    • $allowedTransitions: 定义状态转换规则。

    优点:

    • 适用于有状态变化的操作。
    • 可以保证状态的正确性。

    缺点:

    • 需要定义状态和状态转换规则。
    • 实现较为复杂。
  1. 记录请求日志

记录每次请求的详细信息,例如请求参数、请求时间、客户端IP等。在处理请求之前,先检查是否已经存在相同的请求。如果存在,则直接返回上次的结果;否则执行业务逻辑,并记录请求日志。

  • 流程:

    1. 客户端发起POST/PUT请求。
    2. 服务器端接收到请求后,根据请求参数生成唯一标识符。
    3. 查询请求日志,判断是否存在相同的请求。
    4. 如果存在,则直接返回上次的结果。
    5. 如果不存在,则执行业务逻辑,记录请求日志,并返回结果.
  • PHP代码示例:

    <?php
    
    class ApiService
    {
        private $pdo;
    
        public function __construct(PDO $pdo)
        {
            $this->pdo = $pdo;
        }
    
        /**
         * 处理API请求
         * @param string $endpoint API接口地址
         * @param array $params 请求参数
         * @return array
         */
        public function processApiRequest(string $endpoint, array $params): array
        {
            // 1. 生成请求ID
            $requestId = $this->generateRequestId($endpoint, $params);
    
            // 2. 检查请求日志
            $log = $this->getRequestLog($requestId);
    
            if ($log) {
                // 3. 如果存在相同的请求,则返回上次的结果
                return json_decode($log['response'], true);
            }
    
            // 4. 执行业务逻辑
            $result = $this->executeBusinessLogic($endpoint, $params);
    
            // 5. 记录请求日志
            $this->logRequest($requestId, $endpoint, $params, $result);
    
            return $result;
        }
    
        /**
         * 生成请求ID
         * @param string $endpoint API接口地址
         * @param array $params 请求参数
         * @return string
         */
        private function generateRequestId(string $endpoint, array $params): string
        {
            // 可以使用md5或sha1算法,将endpoint和params组合成唯一ID
            $data = $endpoint . json_encode($params);
            return md5($data);
        }
    
        /**
         * 获取请求日志
         * @param string $requestId 请求ID
         * @return array|null
         */
        private function getRequestLog(string $requestId): ?array
        {
            $sql = "SELECT id, endpoint, params, response, created_at FROM request_logs WHERE request_id = :request_id";
            $stmt = $this->pdo->prepare($sql);
            $stmt->execute(['request_id' => $requestId]);
            $log = $stmt->fetch(PDO::FETCH_ASSOC);
            return $log ?: null;
        }
    
        /**
         * 记录请求日志
         * @param string $requestId 请求ID
         * @param string $endpoint API接口地址
         * @param array $params 请求参数
         * @param array $result 执行结果
         */
        private function logRequest(string $requestId, string $endpoint, array $params, array $result): void
        {
            $sql = "INSERT INTO request_logs (request_id, endpoint, params, response, created_at) VALUES (:request_id, :endpoint, :params, :response, NOW())";
            $stmt = $this->pdo->prepare($sql);
            $stmt->execute([
                'request_id' => $requestId,
                'endpoint' => $endpoint,
                'params' => json_encode($params),
                'response' => json_encode($result),
            ]);
        }
    
        /**
         * 执行业务逻辑 (示例)
         * @param string $endpoint API接口地址
         * @param array $params 请求参数
         * @return array
         */
        private function executeBusinessLogic(string $endpoint, array $params): array
        {
            // 模拟业务逻辑
            // 在实际应用中,你需要根据endpoint和params执行相应的业务逻辑
            return ['status' => 'success', 'message' => 'Request processed', 'data' => $params];
        }
    }
    
    // 使用示例
    try {
        // 假设你已经配置好了PDO连接
        $pdo = new PDO("mysql:host=localhost;dbname=test", "username", "password");
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    
        $apiService = new ApiService($pdo);
    
        // 1. 第一次请求
        $endpoint = '/api/products';
        $params = ['name' => 'Product A', 'price' => 100];
        $response = $apiService->processApiRequest($endpoint, $params);
        print_r($response);
    
        // 2. 模拟重复请求
        echo "nSimulating a duplicate request...n";
        $response2 = $apiService->processApiRequest($endpoint, $params);
        print_r($response2);
    
    } catch (PDOException $e) {
        echo "Database connection failed: " . $e->getMessage();
    }
    ?>

    数据库表结构:

    CREATE TABLE request_logs (
        id INT AUTO_INCREMENT PRIMARY KEY,
        request_id VARCHAR(255) UNIQUE NOT NULL,  -- 请求ID,唯一索引
        endpoint VARCHAR(255) NOT NULL,  -- API接口地址
        params TEXT NOT NULL,            -- 请求参数 (JSON格式)
        response TEXT NOT NULL,          -- 响应结果 (JSON格式)
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP  -- 请求时间
    );

    代码解释:

    • processApiRequest(): 处理API请求。
    • generateRequestId(): 生成请求ID,使用md5算法将API接口地址和请求参数组合成唯一ID。
    • getRequestLog(): 查询请求日志,判断是否存在相同的请求。
    • logRequest(): 记录请求日志。
    • executeBusinessLogic(): 执行业务逻辑。

    优点:

    • 适用于各种场景。
    • 可以记录请求的详细信息,方便排查问题。

    缺点:

    • 需要额外的存储空间来存储请求日志。
    • 性能较低,需要查询数据库。
    • 请求ID的生成方式需要根据具体业务场景进行选择。

策略对比

策略 优点 缺点 适用场景
Token机制 简单易实现,适用于各种场景,可以设置Token的过期时间,防止Token无限期占用资源。 需要额外的存储空间来存储Token,客户端需要先请求Token,增加了一次网络请求。 适用于各种场景,包括创建、更新等操作。尤其适用于需要较高安全性的场景,例如支付接口。
唯一性约束 实现简单,不需要额外的存储空间,性能较好,直接利用数据库的索引。 只适用于创建操作,不适用于更新操作,需要数据库支持唯一性约束,错误判断逻辑可能依赖于具体的数据库实现。 适用于创建操作,例如创建订单、创建用户等,并且数据库中有唯一性约束的字段。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注