本文目录导读:

在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锁 + 重试机制
记住一个原则:锁的超时时间应该大于业务处理的最大时间,但不宜过长,避免影响其他请求。