本文目录导读:

在PHP API开发中,任务去重是一个常见且重要的问题,下面我介绍几种常用的实现方案:
基于幂等性设计(推荐)
唯一请求ID(幂等键)
class IdempotentApi
{
private $redis;
public function __construct($redis)
{
$this->redis = $redis;
}
/**
* 处理幂等请求
* @param string $idempotentKey 客户端生成的唯一键
* @param callable $callback 实际业务处理逻辑
* @param int $ttl 过期时间(秒)
*/
public function handle(string $idempotentKey, callable $callback, int $ttl = 3600)
{
// 检查是否已处理
$result = $this->redis->get("idempotent:{$idempotentKey}");
if ($result !== null) {
// 返回之前的结果
return unserialize($result);
}
// 使用SET NX实现原子操作
$lockKey = "lock:{$idempotentKey}";
$lockAcquired = $this->redis->set(
$lockKey,
'processing',
['NX', 'EX' => 30] // 30秒锁超时
);
if (!$lockAcquired) {
throw new \RuntimeException('请求正在处理中');
}
try {
// 执行实际业务逻辑
$result = $callback();
// 缓存结果
$this->redis->setex(
"idempotent:{$idempotentKey}",
$ttl,
serialize($result)
);
return $result;
} finally {
// 释放锁
$this->redis->del($lockKey);
}
}
}
// 使用示例
$api = new IdempotentApi($redis);
// 客户端需要生成唯一的idempotent_key
$idempotentKey = $_SERVER['HTTP_X_IDEMPOTENT_KEY'] ?? uniqid();
$result = $api->handle($idempotentKey, function() {
// 实际的业务处理
return createOrder($data);
});
基于数据库的唯一约束
MySQL实现
class DuplicateCheckApi
{
private $pdo;
/**
* 数据库表结构:
* CREATE TABLE task_queue (
* id INT AUTO_INCREMENT PRIMARY KEY,
* task_key VARCHAR(64) NOT NULL UNIQUE,
* task_data JSON,
* status ENUM('pending', 'processing', 'completed', 'failed') DEFAULT 'pending',
* result JSON,
* created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
* updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
* );
*/
public function submitTask(string $taskKey, array $taskData)
{
try {
// 使用INSERT IGNORE或ON DUPLICATE KEY
$stmt = $this->pdo->prepare(
'INSERT INTO task_queue (task_key, task_data, status)
VALUES (?, ?, \'pending\')
ON DUPLICATE KEY UPDATE id=id'
);
$stmt->execute([$taskKey, json_encode($taskData)]);
if ($stmt->rowCount() === 0) {
// 任务已存在,返回已有任务信息
return $this->getTaskStatus($taskKey);
}
// 新任务,处理业务逻辑
return $this->processTask($taskKey);
} catch (\PDOException $e) {
// 处理并发冲突
if ($e->getCode() == 23000) { // 重复键异常
return $this->getTaskStatus($taskKey);
}
throw $e;
}
}
}
基于消息队列去重
RabbitMQ + Redis去重
class MessageDeduplicator
{
private $redis;
private $queue;
public function publishDeduplicated(string $messageId, array $messageData)
{
// 使用HyperLogLog或Set进行去重
$isDuplicate = $this->redis->sadd('msg_set', $messageId);
if ($isDuplicate === false) {
// 已存在的消息
return false;
}
// 设置过期时间,避免内存溢出
$this->redis->expire('msg_set', 86400); // 24小时
// 发布到消息队列
$this->queue->publish(json_encode([
'id' => $messageId,
'data' => $messageData,
'timestamp' => time()
]));
return true;
}
}
基于时间窗口去重
class TimeWindowDeduplicator
{
private $redis;
private $windowSize = 60; // 60秒窗口
/**
* 基于滑动时间窗口去重
*/
public function isDuplicate(string $taskId): bool
{
$key = "task_window:{$taskId}";
$now = time();
// 使用有序集合,时间戳作为score
$this->redis->zadd($key, $now, "{$now}:{$taskId}");
// 移除窗口外的数据
$this->redis->zremrangebyscore($key, 0, $now - $this->windowSize);
// 检查窗口内是否有重复
$count = $this->redis->zcard($key);
return $count > 1;
}
}
综合实现示例
class TaskDeduplicationService
{
private $redis;
private $db;
private $lockTimeout = 30;
private $cacheTimeout = 3600;
/**
* 完整的去重处理流程
*/
public function processTask(string $taskId, array $taskData, callable $handler)
{
// 1. 幂等性检查
$cacheKey = "task_result:{$taskId}";
$cachedResult = $this->redis->get($cacheKey);
if ($cachedResult !== null) {
return unserialize($cachedResult);
}
// 2. 分布式锁 - 防止并发
$lockKey = "task_lock:{$taskId}";
$lockToken = uniqid('', true);
$locked = $this->redis->set(
$lockKey,
$lockToken,
['NX', 'EX' => $this->lockTimeout]
);
if (!$locked) {
// 等待重试或返回冲突
return $this->handleConcurrency($taskId);
}
try {
// 3. 数据库幂等检查
if ($this->isTaskProcessed($taskId)) {
return $this->getTaskResult($taskId);
}
// 4. 执行任务
$result = $handler($taskData);
// 5. 标记任务完成
$this->markTaskCompleted($taskId, $result);
// 6. 缓存结果
$this->redis->setex(
$cacheKey,
$this->cacheTimeout,
serialize($result)
);
return $result;
} catch (\Exception $e) {
$this->handleError($taskId, $e);
throw $e;
} finally {
// 7. 释放锁(使用Lua脚本保证原子性)
$this->releaseLock($lockKey, $lockToken);
}
}
private function releaseLock(string $key, string $token)
{
$script = <<<LUA
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
LUA;
$this->redis->eval($script, [$key, $token], 1);
}
private function handleConcurrency(string $taskId)
{
// 重试逻辑
$maxRetries = 3;
$retryDelay = 100; // 毫秒
for ($i = 0; $i < $maxRetries; $i++) {
usleep($retryDelay * 1000);
$cachedResult = $this->redis->get("task_result:{$taskId}");
if ($cachedResult !== null) {
return unserialize($cachedResult);
}
}
throw new \RuntimeException("任务{$taskId}正在处理中,请稍后重试");
}
}
客户端实现建议
// 客户端生成唯一ID
class ClientIdGenerator
{
public static function generateRequestId(): string
{
// UUID v4
$data = random_bytes(16);
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}
}
// 客户端调用示例
$client = new GuzzleHttp\Client();
$requestId = ClientIdGenerator::generateRequestId();
$response = $client->post('/api/orders', [
'json' => $data,
'headers' => [
'X-Idempotent-Key' => $requestId,
'X-Request-Id' => $requestId
]
]);
推荐方案选择
| 场景 | 推荐方案 | 优点 | 缺点 |
|---|---|---|---|
| 高并发API | 幂等性+Redis | 性能好,原子操作 | 需要Redis |
| 数据一致性要求高 | 数据库唯一约束 | 强一致性 | 性能相对较低 |
| 分布式系统 | 消息队列+去重 | 解耦,可扩展 | 复杂度高 |
| 简单场景 | Token机制 | 实现简单 | 需要额外存储 |
最佳实践建议
- 客户端生成唯一ID,服务端不做ID生成
- 使用分布式锁防止并发问题
- 设置合理的TTL,避免存储无限增长
- 考虑幂等性和去重的关系,幂等允许重复请求但结果一致
- 做好日志记录,方便问题排查
- 返回清晰的状态码,如409 Conflict表示重复请求
选择哪种方案取决于你的具体业务场景、并发量和系统架构。