本文目录导读:

在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
# 自动释放锁(脚本退出时)
注意事项
- 锁的过期时间:对于文件锁,考虑设置超时机制,防止死锁
- 异常处理:确保在异常情况下也能释放锁
- 并发环境:分布式环境下使用Redis或数据库锁更可靠
- 锁的粒度:确保锁的粒度合适,避免影响其他操作
推荐方案
- 单机环境:使用文件锁(flock)
- 多服务器环境:使用Redis锁或数据库锁
- 简单的cron任务:使用标志文件或PID文件
选择哪种方案取决于你的具体场景、系统架构和可靠性要求。