本文目录导读:

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