PHP如何避免脚本重复执行

wen PHP项目 1

本文目录导读:

PHP如何避免脚本重复执行

  1. 使用文件锁(推荐)
  2. 使用数据库锁
  3. 使用标志文件(简单但不够可靠)
  4. 使用Redis锁(分布式环境下推荐)
  5. 使用命令行工具(cron场景)
  6. 注意事项
  7. 推荐方案

在PHP中避免脚本重复执行有多种方法,以下是几种常用的解决方案:

使用文件锁(推荐)

<?php
class SingleScript {
    private $lockFile;
    private $fp;
    public function __construct($name = 'default') {
        // 锁文件放在临时目录
        $this->lockFile = sys_get_temp_dir() . '/' . md5($name) . '.lock';
    }
    public function acquire() {
        $this->fp = fopen($this->lockFile, 'w');
        // 尝试获取独占锁(非阻塞模式)
        if (!flock($this->fp, LOCK_EX | LOCK_NB)) {
            fclose($this->fp);
            return false;
        }
        // 写入当前进程PID和时间戳
        fwrite($this->fp, "PID: " . getmypid() . " Time: " . date('Y-m-d H:i:s'));
        fflush($this->fp);
        return true;
    }
    public function release() {
        if ($this->fp) {
            flock($this->fp, LOCK_UN);
            fclose($this->fp);
        }
    }
    public function __destruct() {
        $this->release();
    }
}
// 使用示例
$lock = new SingleScript('my_task');
if (!$lock->acquire()) {
    echo "脚本已在运行中,退出\n";
    exit(1);
}
// 业务逻辑
echo "开始执行任务...\n";
sleep(10); // 模拟耗时的任务
echo "任务完成\n";
// 释放锁(可选,析构函数会自动释放)
$lock->release();
?>

使用数据库锁

MySQL的GET_LOCK函数

<?php
function acquireDbLock($lockName, $timeout = 0) {
    $pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'password');
    // 获取互斥锁
    $stmt = $pdo->prepare("SELECT GET_LOCK(?, ?)");
    $stmt->execute([$lockName, $timeout]);
    $result = $stmt->fetchColumn();
    if ($result == 1) {
        // 锁定成功
        return function() use ($pdo, $lockName) {
            $stmt = $pdo->prepare("SELECT RELEASE_LOCK(?)");
            $stmt->execute([$lockName]);
        };
    }
    return null;
}
// 使用示例
$releaseLock = acquireDbLock('cron_task', 0);
if (!$releaseLock) {
    echo "任务已在运行\n";
    exit(1);
}
// 执行业务逻辑
try {
    echo "开始任务...\n";
    // 任务代码
    $releaseLock(); // 释放锁
} catch (Exception $e) {
    echo "错误: " . $e->getMessage();
    $releaseLock(); // 确保释放锁
}
?>

使用标志文件(简单但不够可靠)

<?php
function checkAndSetFlag($flagFile, $maxAge = 300) {
    // 检查标志文件是否存在且未过期
    if (file_exists($flagFile)) {
        $age = time() - filemtime($flagFile);
        if ($age < $maxAge) {
            return false; // 任务仍在运行
        }
    }
    // 创建标志文件
    if (!file_put_contents($flagFile, date('Y-m-d H:i:s'))) {
        return false;
    }
    return true;
}
// 使用示例
$flagFile = '/tmp/my_task.flag';
if (!checkAndSetFlag($flagFile, 300)) { // 10分钟超时
    echo "任务已在运行\n";
    exit(1);
}
// 任务完成后删除标志文件
register_shutdown_function(function() use ($flagFile) {
    @unlink($flagFile);
});
// 业务逻辑
echo "开始执行任务...\n";
// 任务代码
?>

使用Redis锁(分布式环境下推荐)

<?php
class RedisLock {
    private $redis;
    private $lockKey;
    public function __construct($lockKey, $host = '127.0.0.1', $port = 6379) {
        $this->redis = new Redis();
        $this->redis->connect($host, $port);
        $this->lockKey = $lockKey;
    }
    public function acquire($timeout = 10, $expire = 30) {
        $token = uniqid();
        $result = $this->redis->set(
            $this->lockKey, 
            $token, 
            ['NX', 'EX' => $expire]
        );
        if ($result) {
            $this->token = $token;
            return true;
        }
        // 等待重试
        while ($timeout > 0) {
            sleep(1);
            $timeout--;
            $result = $this->redis->set(
                $this->lockKey, 
                $token, 
                ['NX', 'EX' => $expire]
            );
            if ($result) {
                $this->token = $token;
                return true;
            }
        }
        return false;
    }
    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->token], 1);
    }
}
// 使用示例
$lock = new RedisLock('cron_task_lock');
if (!$lock->acquire(0)) {
    echo "任务已在运行\n";
    exit(1);
}
// 业务逻辑
try {
    echo "开始执行任务...\n";
    // 任务代码
} finally {
    $lock->release();
}
?>

使用命令行工具(cron场景)

#!/bin/bash
# 使用fuser检查进程是否在运行
LOCK_FILE="/tmp/my_script.lock"
if fuser "$LOCK_FILE" > /dev/null 2>&1; then
    echo "脚本已在运行"
    exit 1
fi
# 创建锁文件
exec 200>"$LOCK_FILE"
flock -n 200 || exit 1
# 执行PHP脚本
php /path/to/script.php
# 自动释放锁(脚本退出时)

注意事项

  1. 锁的过期时间:对于文件锁,考虑设置超时机制,防止死锁
  2. 异常处理:确保在异常情况下也能释放锁
  3. 并发环境:分布式环境下使用Redis或数据库锁更可靠
  4. 锁的粒度:确保锁的粒度合适,避免影响其他操作

推荐方案

  • 单机环境:使用文件锁(flock)
  • 多服务器环境:使用Redis锁或数据库锁
  • 简单的cron任务:使用标志文件或PID文件

选择哪种方案取决于你的具体场景、系统架构和可靠性要求。

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