本文目录导读:

在PHP项目中,规避定时任务重复执行主要有以下几种常用方案:
基于文件锁(最常用)
<?php
class TaskLocker {
private $lockFile;
private $fp;
public function __construct($taskName) {
$this->lockFile = sys_get_temp_dir() . '/' . md5($taskName) . '.lock';
}
public function acquire() {
$this->fp = fopen($this->lockFile, 'c');
if (flock($this->fp, LOCK_EX | LOCK_NB)) {
return true; // 获取锁成功
}
return false; // 获取锁失败,任务正在运行
}
public function release() {
if ($this->fp) {
flock($this->fp, LOCK_UN);
fclose($this->fp);
@unlink($this->lockFile);
}
}
}
// 使用示例
$locker = new TaskLocker('sync_order_task');
if (!$locker->acquire()) {
echo "任务正在运行中\n";
exit;
}
try {
// 执行业务逻辑
echo "执行定时任务...\n";
sleep(10); // 模拟耗时操作
} finally {
$locker->release();
}
基于数据库锁
<?php
class DatabaseTaskLocker {
private $db;
private $taskName;
private $lockTime = 300; // 锁超时时间(秒)
public function __construct($taskName) {
$this->taskName = $taskName;
$this->db = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
}
public function acquire() {
try {
// 使用MySQL GET_LOCK() 函数
$stmt = $this->db->prepare("SELECT GET_LOCK(:task_name, 0) as locked");
$stmt->execute([':task_name' => $this->taskName]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
return $result['locked'] == 1;
} catch (Exception $e) {
return false;
}
}
public function release() {
$stmt = $this->db->prepare("SELECT RELEASE_LOCK(:task_name)");
$stmt->execute([':task_name' => $this->taskName]);
}
}
// 或使用表记录方式
class TaskLockByTable {
private $db;
public function tryLock($taskName, $timeout = 300) {
$expireTime = date('Y-m-d H:i:s', time() + $timeout);
// 尝试插入锁记录,如果已存在则更新失败
$sql = "INSERT INTO task_locks (task_name, expire_time, created_at)
VALUES (:task_name, :expire_time, NOW())
ON DUPLICATE KEY UPDATE
expire_time = IF(expire_time < NOW(), :expire_time2, expire_time)";
$stmt = $this->db->prepare($sql);
$stmt->execute([
':task_name' => $taskName,
':expire_time' => $expireTime,
':expire_time2' => $expireTime
]);
return $stmt->rowCount() > 0;
}
public function release($taskName) {
$this->db->prepare("DELETE FROM task_locks WHERE task_name = :task_name")
->execute([':task_name' => $taskName]);
}
}
基于Redis锁(推荐用于分布式环境)
<?php
class RedisTaskLocker {
private $redis;
private $lockKey;
private $lockValue;
private $ttl = 300; // 锁超时时间
public function __construct($taskName) {
$this->redis = new Redis();
$this->redis->connect('127.0.0.1', 6379);
$this->lockKey = 'task_lock:' . $taskName;
$this->lockValue = uniqid('', true);
}
public function acquire() {
// SET NX EX 原子操作
return $this->redis->set(
$this->lockKey,
$this->lockValue,
['NX', 'EX' => $this->ttl]
);
}
public function release() {
// 使用Lua脚本保证原子性,只释放自己的锁
$script = "
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end";
return $this->redis->eval($script, [$this->lockKey, $this->lockValue], 1);
}
}
// 使用示例
$locker = new RedisTaskLocker('sync_order_task');
if (!$locker->acquire()) {
echo "任务正在其他实例上运行\n";
exit;
}
try {
// 执行业务逻辑
echo "执行定时任务...\n";
} finally {
$locker->release();
}
使用缓存(如Redis)记录任务执行状态
<?php
class TaskExecutionTracker {
private $redis;
public function shouldExecute($taskName, $interval = 60) {
$key = 'task_last_run:' . $taskName;
$lastRun = $this->redis->get($key);
if ($lastRun && (time() - $lastRun) < $interval) {
return false; // 间隔内已执行
}
// 更新执行时间
$this->redis->setex($key, $interval, time());
return true;
}
}
// 使用示例
$tracker = new TaskExecutionTracker();
if (!$tracker->shouldExecute('notify_users', 300)) { // 5分钟执行一次
echo "该任务已执行,等待下次执行\n";
exit;
}
基于crontab的自我保护
# crontab 配置,每隔5分钟执行一次 */5 * * * * /usr/bin/php /path/to/task.php # 如果任务执行时间超过5分钟,下次执行会自动跳过
// task.php 中判断进程数量
<?php
function isProcessRunning($processName) {
exec("ps aux | grep '{$processName}' | grep -v grep", $output);
return count($output) > 1; // 包含当前进程
}
$processName = 'order_sync';
if (isProcessRunning($processName)) {
echo "进程已存在,退出\n";
exit;
}
最佳实践建议
-
选择合适方案:
- 单机环境:文件锁(最简单)
- 分布式环境:Redis锁或数据库锁
- 对一致性要求高:Redis + Lua脚本
-
设置合理的锁超时时间:
// 锁超时时间应大于任务最大执行时间 $lockTimeout = maxExpectedExecutionTime * 2;
-
异常处理:
try { // 获取锁 // 执行业务 } catch (Exception $e) { // 记录日志 error_log("Task failed: " . $e->getMessage()); } finally { // 释放锁 $locker->release(); } -
监控告警:
// 记录任务执行状态到监控系统 class TaskMonitor { public static function recordExecution($taskName, $status, $duration) { // 发送到监控系统(如 Prometheus、Grafana) } }
推荐使用 Redis锁 或 文件锁 方案,它们实现简单且可靠,如果使用多台服务器执行任务,必须使用分布式锁(Redis或数据库)。