PHP项目定时任务重复执行如何规避

wen PHP项目 25

本文目录导读:

PHP项目定时任务重复执行如何规避

  1. 基于文件锁(最常用)
  2. 基于数据库锁
  3. 基于Redis锁(推荐用于分布式环境)
  4. 使用缓存(如Redis)记录任务执行状态
  5. 基于crontab的自我保护
  6. 最佳实践建议

在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;
}

最佳实践建议

  1. 选择合适方案

    • 单机环境:文件锁(最简单)
    • 分布式环境:Redis锁或数据库锁
    • 对一致性要求高:Redis + Lua脚本
  2. 设置合理的锁超时时间

    // 锁超时时间应大于任务最大执行时间
    $lockTimeout = maxExpectedExecutionTime * 2;
  3. 异常处理

    try {
        // 获取锁
        // 执行业务
    } catch (Exception $e) {
        // 记录日志
        error_log("Task failed: " . $e->getMessage());
    } finally {
        // 释放锁
        $locker->release();
    }
  4. 监控告警

    // 记录任务执行状态到监控系统
    class TaskMonitor {
        public static function recordExecution($taskName, $status, $duration) {
            // 发送到监控系统(如 Prometheus、Grafana)
        }
    }

推荐使用 Redis锁文件锁 方案,它们实现简单且可靠,如果使用多台服务器执行任务,必须使用分布式锁(Redis或数据库)。

抱歉,评论功能暂时关闭!