本文目录导读:

在 PHP 中实现延迟消息的取消,主要有以下几种方案,我会从简单到复杂逐一介绍:
Redis + 延迟队列(推荐方案)
原理
使用 Redis 的有序集合(ZSet),通过 score 存储执行时间戳,通过一个独立进程扫描到期任务。
<?php
class DelayedQueue {
private $redis;
private $queueKey = 'delayed:queue';
private $cancelKey = 'delayed:cancel';
public function __construct($redis) {
$this->redis = $redis;
}
/**
* 添加延迟任务
*/
public function add(string $taskId, array $data, int $delaySeconds): bool {
$executeTime = time() + $delaySeconds;
$task = json_encode([
'id' => $taskId,
'data' => $data,
'create_time' => time()
]);
// 将任务添加到 ZSet,score 为执行时间戳
return $this->redis->zAdd($this->queueKey, $executeTime, $task);
}
/**
* 取消延迟任务
*/
public function cancel(string $taskId): bool {
// 将所有该 taskId 的任务标记为取消
$cancelData = json_encode([
'id' => $taskId,
'cancel_time' => time()
]);
return $this->redis->hSet($this->cancelKey, $taskId, $cancelData);
}
/**
* 检查任务是否被取消(消费者处理时调用)
*/
public function isCancelled(string $taskId): bool {
return $this->redis->hExists($this->cancelKey, $taskId);
}
/**
* 获取到期任务(消费者轮询)
*/
public function pollReadyTasks(): array {
$now = time();
$tasks = $this->redis->zRangeByScore(
$this->queueKey,
'-inf',
$now,
['limit' => [0, 100]]
);
$readyTasks = [];
foreach ($tasks as $taskJson) {
$task = json_decode($taskJson, true);
// 检查是否被取消
if (!$this->isCancelled($task['id'])) {
$readyTasks[] = $task;
// 从队列中移除
$this->redis->zRem($this->queueKey, $taskJson);
} else {
// 被取消的任务直接移除
$this->redis->zRem($this->queueKey, $taskJson);
$this->redis->hDel($this->cancelKey, $task['id']);
}
}
return $readyTasks;
}
/**
* 清理已过期的取消标记
*/
public function cleanupCancelMarkers(int $maxAge = 86400): int {
$count = 0;
$cancelTasks = $this->redis->hGetAll($this->cancelKey);
foreach ($cancelTasks as $taskId => $cancelData) {
$data = json_decode($cancelData, true);
if (time() - $data['cancel_time'] > $maxAge) {
$this->redis->hDel($this->cancelKey, $taskId);
$count++;
}
}
return $count;
}
}
消费者处理
<?php
// consumer.php - 独立进程运行
while (true) {
$queue = new DelayedQueue($redis);
// 获取到期的任务
$readyTasks = $queue->pollReadyTasks();
foreach ($readyTasks as $task) {
try {
// 执行任务
processTask($task);
} catch (Exception $e) {
// 记录错误日志
error_log("Task {$task['id']} failed: " . $e->getMessage());
}
}
// 定期清理取消标记
if (time() % 3600 == 0) {
$queue->cleanupCancelMarkers();
}
// 每100毫秒轮询一次
usleep(100000);
}
Redis + 流(Stream)方案
Redis 5.0+ 的 Stream 数据结构,支持消息确认和待处理列表。
<?php
class DelayedStreamQueue {
private $redis;
private $streamKey = 'task:stream';
private $groupName = 'task:group';
public function __construct($redis) {
$this->redis = $redis;
}
/**
* 添加延迟任务
*/
public function add(string $taskId, array $data, int $delaySeconds): string {
$executeTime = time() + $delaySeconds;
$messageId = $this->redis->xAdd($this->streamKey, '*', [
'task_id' => $taskId,
'data' => json_encode($data),
'execute_time' => $executeTime,
'status' => 'pending'
]);
return $messageId;
}
/**
* 取消任务 - 标记为已取消
*/
public function cancel(string $taskId): bool {
// 使用 Lua 脚本来原子性更新所有匹配的任务
$luaScript = <<<LUA
local stream = KEYS[1]
local taskId = ARGV[1]
local cancelTime = ARGV[2]
-- 获取流中的信息
local entries = redis.call('XRANGE', stream, '-', '+')
local updated = 0
for _, entry in ipairs(entries) do
local msgId = entry[1]
local fields = entry[2]
local task_id = fields[2] -- 这里需要根据实际结构调整
if task_id == taskId then
-- 更新状态为已取消
redis.call('XACK', stream, KEYS[2], msgId)
redis.call('XDEL', stream, msgId)
updated = updated + 1
end
end
return updated
LUA;
return $this->redis->eval($luaScript, 2, $this->streamKey, $this->groupName, $taskId, time());
}
}
RabbitMQ 延迟消息方案
使用 TTL + 死信队列
<?php
class RabbitMQDelayedQueue {
private $connection;
private $channel;
public function __construct($connection) {
$this->connection = $connection;
$this->channel = $connection->channel();
$this->setup();
}
private function setup() {
// 声明主队列(延迟队列)
$this->channel->queue_declare(
'delay_queue', // 队列名
false, // passive
true, // durable
false, // exclusive
false, // auto_delete
false, // no_wait
[
'x-dead-letter-exchange' => ['exchange', 'task.exchange'],
'x-dead-letter-routing-key' => ['routing_key', 'task.process'],
'x-message-ttl' => ['I', 5000] // 5秒延迟
]
);
// 声明处理队列
$this->channel->queue_declare(
'process_queue',
false,
true,
false,
false
);
// 声明交换机
$this->channel->exchange_declare('task.exchange', 'direct', false, true, false);
// 绑定
$this->channel->queue_bind('process_queue', 'task.exchange', 'task.process');
}
/**
* 发布延迟消息
*/
public function addTask(string $taskId, array $data, int $delayMs) {
$this->channel->queue_declare(
'delay_queue_' . $taskId, // 每个任务一个队列来动态设置 TTL
false,
true,
false,
false,
false,
[
'x-dead-letter-exchange' => ['exchange', 'task.exchange'],
'x-dead-letter-routing-key' => ['routing_key', 'task.process'],
'x-message-ttl' => ['I', $delayMs]
]
);
$message = new AMQPMessage(json_encode([
'task_id' => $taskId,
'data' => $data
]));
$this->channel->basic_publish(
$message,
'',
'delay_queue_' . $taskId
);
}
/**
* 取消延迟任务
*/
public function cancelTask(string $taskId) {
// 直接删除对应的延迟队列
$this->channel->queue_delete('delay_queue_' . $taskId);
}
}
数据库定时扫描方案
适用于中小型项目,使用数据库存储任务状态。
<?php
class DatabaseDelayQueue {
private $pdo;
/**
* 添加延迟任务
*/
public function add(string $taskId, array $data, int $delaySeconds): bool {
$stmt = $this->pdo->prepare(
'INSERT INTO delayed_tasks (task_id, data, execute_at, status, created_at)
VALUES (?, ?, DATE_ADD(NOW(), INTERVAL ? SECOND), ?, NOW())'
);
return $stmt->execute([
$taskId,
json_encode($data, JSON_UNESCAPED_UNICODE),
$delaySeconds,
'pending'
]);
}
/**
* 取消任务
*/
public function cancel(string $taskId): bool {
$stmt = $this->pdo->prepare(
'UPDATE delayed_tasks
SET status = ?, cancelled_at = NOW()
WHERE task_id = ? AND status = ?
AND execute_at > NOW()'
);
return $stmt->execute(['cancelled', $taskId, 'pending']);
}
/**
* 获取并执行到期任务
*/
public function processDueTasks(): array {
$now = new DateTime();
$stmt = $this->pdo->prepare(
'SELECT task_id, data
FROM delayed_tasks
WHERE execute_at <= ? AND status = ?
FOR UPDATE SKIP LOCKED'
);
$stmt->execute([$now->format('Y-m-d H:i:s'), 'pending']);
$tasks = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 更新任务状态为执行中
foreach ($tasks as $task) {
$this->pdo->prepare(
'UPDATE delayed_tasks SET status = ? WHERE task_id = ?'
)->execute(['processing', $task['task_id']]);
}
return $tasks;
}
/**
* 完成任务
*/
public function complete(string $taskId): bool {
$stmt = $this->pdo->prepare(
'UPDATE delayed_tasks
SET status = ?, completed_at = NOW()
WHERE task_id = ?'
);
return $stmt->execute(['completed', $taskId]);
}
}
其他高级方案
使用 Redis RStream 扩展
// 使用 RedStream 或 RedisStreams 库
$stream = new RedisStreams\Stream($redis, 'tasks');
// 添加带延迟的任务
$stream->add([
'task_id' => $taskId,
'data' => json_encode($data),
'execute_at' => time() + $delaySeconds
], $maxLen);
// 取消任务
$stream->query(
RedisStreams\Query::greaterThan($lastId)
);
使用 PECL 插件
// pecl 安装 redis 扩展后 $queue = new \RedisQueue\DelayedQueue($redis); $queue->addJob($taskId, $data, $delaySeconds); $queue->cancelJob($taskId);
选择建议
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Redis ZSet | 实现简单、性能高、支持高效取消 | 需要额外消费者进程 | 多数场景,推荐 |
| Redis Stream | 持久化、支持消费组、可追溯 | 实现复杂度较高 | 需要消息确认机制 |
| RabbitMQ | 专业 MQ、可靠性高、支持 TTL | 部署复杂、资源占用大 | 企业级应用 |
| 数据库轮询 | 简单直观、事务支持好 |
版权声明: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直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播