PHPAPI幂等性怎么保证

wen PHP项目 3

本文目录导读:

PHPAPI幂等性怎么保证

  1. 幂等性令牌(Idempotency Key)方案(推荐)
  2. 数据库唯一约束方案
  3. Redis + Lua 脚本(原子操作)
  4. 分布式锁方案
  5. 幂等性最佳实践

在 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;
    }
}
  1. 首选方案:Idempotency Key + Redis 缓存 + 数据库事务
  2. 核心原则:客户端生成唯一 Key,服务端记录并校验
  3. 重要考虑
    • Key 的过期时间(建议 24 小时)
    • 原子操作防止竞态条件
    • 错误处理(超时、服务重启等)
    • 日志记录以便问题排查

选择哪种方案取决于你的业务场景、并发量、数据一致性要求等因素,对于金融类接口,建议使用数据库事务 + 唯一约束 + 幂等性令牌的组合方案。

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