PHPAPI任务防重入怎么实现

wen PHP项目 13

本文目录导读:

PHPAPI任务防重入怎么实现

  1. 基于Redis的分布式锁(推荐)
  2. 基于数据库的唯一约束
  3. 使用请求唯一标识(幂等性)
  4. 基于文件锁(适用于单机)
  5. 基于消息队列的防重
  6. 中间件封装(推荐结构)
  7. 最佳实践建议

在PHP API中实现任务防重入(防止重复提交/并发处理相同请求),常见方案有以下几种:

基于Redis的分布式锁(推荐)

使用SET NX + EX(原子操作)

class RedisLock {
    private $redis;
    private $prefix = 'lock:';
    public function __construct($redis) {
        $this->redis = $redis;
    }
    /**
     * 获取锁
     * @param string $key 任务标识
     * @param int $ttl 锁过期时间(秒)
     * @return bool
     */
    public function acquire($key, $ttl = 10) {
        $lockKey = $this->prefix . $key;
        // 使用 Redis SET NX EX 原子操作
        $result = $this->redis->set($lockKey, 1, ['nx', 'ex' => $ttl]);
        return $result === true;
    }
    /**
     * 释放锁
     */
    public function release($key) {
        $lockKey = $this->prefix . $key;
        $this->redis->del($lockKey);
    }
}
// 使用示例
$lock = new RedisLock($redis);
$taskId = 'order_' . $orderId;
if ($lock->acquire($taskId, 30)) {
    try {
        // 处理业务逻辑
        processOrder($orderId);
    } finally {
        $lock->release($taskId);
    }
} else {
    // 任务正在处理中
    throw new \Exception('请求正在处理中,请稍后');
}

使用Redis分布式锁增强版

class EnhancedRedisLock {
    private $redis;
    private $prefix = 'lock:';
    public function acquire($key, $ttl = 10, $retryDelay = 100000, $maxRetries = 3) {
        $lockKey = $this->prefix . $key;
        for ($i = 0; $i < $maxRetries; $i++) {
            // 使用setnx和expire确保原子性操作
            $result = $this->redis->set($lockKey, 1, ['nx', 'ex' => $ttl]);
            if ($result) {
                return true;
            }
            // 检查锁是否过期(处理死锁)
            if ($this->redis->ttl($lockKey) == -1) {
                $this->redis->expire($lockKey, $ttl);
            }
            // 等待重试
            usleep($retryDelay);
        }
        return false;
    }
}

基于数据库的唯一约束

使用MySQL唯一索引实现

class DatabaseLock {
    private $pdo;
    public function tryLock($taskId, $expireTime = '+1 minute') {
        try {
            $this->pdo->beginTransaction();
            // 尝试插入锁记录
            $stmt = $this->pdo->prepare("
                INSERT INTO task_locks (task_id, created_at, expire_at) 
                VALUES (?, NOW(), ?)
            ");
            $stmt->execute([$taskId, date('Y-m-d H:i:s', strtotime($expireTime))]);
            $this->pdo->commit();
            return true;
        } catch (\PDOException $e) {
            // 唯一约束冲突说明已存在锁
            if ($e->errorInfo[1] == 1062) { // MySQL duplicate entry error code
                $this->pdo->rollBack();
                // 检查锁是否过期
                if ($this->isLockExpired($taskId)) {
                    $this->releaseLock($taskId);
                    return $this->tryLock($taskId, $expireTime);
                }
                return false;
            }
            throw $e;
        }
    }
    private function isLockExpired($taskId) {
        $stmt = $this->pdo->prepare("
            SELECT COUNT(*) FROM task_locks 
            WHERE task_id = ? AND expire_at < NOW()
        ");
        $stmt->execute([$taskId]);
        return $stmt->fetchColumn() > 0;
    }
    private function releaseLock($taskId) {
        $stmt = $this->pdo->prepare("DELETE FROM task_locks WHERE task_id = ?");
        $stmt->execute([$taskId]);
    }
}

使用请求唯一标识(幂等性)

基于Token的防重入

class IdempotentHandler {
    private $cache;
    /**
     * 生成请求唯一ID
     */
    public function generateIdempotentKey() {
        return md5(uniqid(mt_rand(), true));
    }
    /**
     * 检查并记录请求
     */
    public function checkAndRecord($key, $ttl = 3600) {
        $cacheKey = 'idempotent:' . $key;
        // 使用Redis SET NX确保唯一性
        $result = $this->cache->set($cacheKey, 1, ['nx', 'ex' => $ttl]);
        if (!$result) {
            // 获取已存在的处理结果
            return [
                'status' => 'duplicate',
                'result' => $this->cache->get($cacheKey . ':result')
            ];
        }
        return [
            'status' => 'new'
        ];
    }
    /**
     * 保存处理结果
     */
    public function saveResult($key, $result) {
        $cacheKey = 'idempotent:' . $key . ':result';
        $this->cache->set($cacheKey, $result, ['ex' => 3600]);
    }
}
// 使用示例
$handler = new IdempotentHandler($redis);
$idempotentKey = $_SERVER['HTTP_X_IDEMPOTENT_KEY'] ?? '';
if ($idempotentKey) {
    $result = $handler->checkAndRecord($idempotentKey);
    if ($result['status'] === 'duplicate') {
        // 返回之前处理的结果
        return json_encode($result['result']);
    }
    try {
        $response = processBusinessLogic();
        $handler->saveResult($idempotentKey, $response);
        return $response;
    } catch (\Exception $e) {
        // 处理失败,删除锁允许重试
        $handler->remove($idempotentKey);
        throw $e;
    }
}

基于文件锁(适用于单机)

class FileLock {
    private $lockDir = '/tmp/locks/';
    private $fp;
    public function acquire($key, $timeout = 10) {
        $lockFile = $this->lockDir . md5($key) . '.lock';
        $this->fp = fopen($lockFile, 'c');
        if (!$this->fp) {
            return false;
        }
        // 尝试获取独占锁(阻塞模式)
        $start = time();
        while (true) {
            if (flock($this->fp, LOCK_EX | LOCK_NB)) {
                return true;
            }
            if (time() - $start >= $timeout) {
                fclose($this->fp);
                return false;
            }
            usleep(100000); // 100ms
        }
    }
    public function release() {
        if ($this->fp) {
            flock($this->fp, LOCK_UN);
            fclose($this->fp);
        }
    }
}

基于消息队列的防重

class MessageQueueLock {
    private $queue;
    public function processWithLock($task, $callback) {
        // 为任务生成唯一消息ID
        $messageId = $this->generateMessageId($task);
        // 使用消息去重特性
        $result = $this->queue->sendWithDedup([
            'id' => $messageId,
            'data' => $task
        ]);
        if ($result === false) {
            // 重复消息,已被处理
            return 'duplicate';
        }
        // 执行回调
        return $callback($task);
    }
}

中间件封装(推荐结构)

class AntiReentrantMiddleware {
    private $lockDriver;
    public function handle($request, \Closure $next) {
        // 生成锁键
        $lockKey = $this->buildLockKey($request);
        // 尝试获取锁
        if (!$this->lockDriver->acquire($lockKey, 30)) {
            return response()->json([
                'code' => 429,
                'message' => '请求正在处理中,请勿重复提交'
            ], 429);
        }
        try {
            $response = $next($request);
            return $response;
        } finally {
            $this->lockDriver->release($lockKey);
        }
    }
    private function buildLockKey($request) {
        // 根据业务需求组合锁键
        $parts = [
            $request->getMethod(),
            $request->getPathInfo(),
            $request->get('order_id', ''),
            $request->get('user_id', '')
        ];
        return 'antireentrant:' . md5(implode(':', $parts));
    }
}

最佳实践建议

锁键设计原则

// 好的锁键设计
$lockKey = sprintf(
    'task:%s:%s:%s',
    $taskType,
    $resourceId,
    $action
);
// 避免使用参数拼接
$lockKey = $request->getUri(); // 不要只用URI

设置合理的超时时间

// 根据业务复杂度设置
const SHORT_TASK_TTL = 5;   // 短任务
const MEDIUM_TASK_TTL = 30; // 中等任务
const LONG_TASK_TTL = 120;  // 长任务

异常处理

try {
    if ($lock->acquire($key)) {
        // 执行业务逻辑
    } else {
        // 友好提示
        throw new BusyException('系统正在处理您的请求,请稍后');
    }
} catch (\Throwable $e) {
    // 确保锁释放
    $lock->release($key);
    throw $e;
}

选择哪种方案取决于你的具体需求:

  • 分布式系统:Redis分布式锁
  • 单机应用:文件锁或数据库锁
  • 要求幂等性:请求唯一标识方案
  • 高并发场景:Redis锁 + 重试机制

记住一个原则:锁的超时时间应该大于业务处理的最大时间,但不宜过长,避免影响其他请求

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