本文目录导读:

在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表示重复请求
选择哪种方案取决于你的具体业务场景、并发量和系统架构。
版权声明: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直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播