运行 多个 PHP 脚本从 cron 作业启动的命令行一次
Running multiple PHP scripts at once from command line initiated by cron jobs
我有一个关于 cron 作业的问题。
这里简单介绍一下情况:
我每分钟调用一个 php 脚本 (*****) (PHP CLI)。
PHP 脚本发出一个数据库请求,以检查在特定分钟内是否有任何电子邮件要发送,并相应地发送它们。
PHP脚本执行需要1分钟以上(庞大的数据库->要发送大量电子邮件->即使使用多线程也需要时间)
同一 PHP 脚本在下一分钟 (*****) 被调用,而它的第一次执行尚未完成。
第一次执行是否会中断,当再次调用脚本时(每分钟调用一次),即使第一次执行尚未完成?
希望你的情况清楚。
操作系统:显然Linux ...
感谢您的帮助!
不,将产生一个新进程。快速测试表明:
shell> cat /root/run_sleep.sh
#!/bin/bash
sleep 10000
shell> crontab -l
* * * * * sh /root/run_sleep.sh
您可以检查 ps
每分钟创建不同的进程:
shell> ps aux | grep run[_]
root 2316 0.0 0.0 113120 1192 ? Ss 22:46 0:00 sh /root/run_sleep.sh
root 2339 0.0 0.0 113120 1188 ? Ss 22:47 0:00 sh /root/run_sleep.sh
root 2363 0.0 0.0 113120 1192 ? Ss 22:48 0:00 sh /root/run_sleep.sh
root 2388 0.0 0.0 113120 1192 ? Ss 22:49 0:00 sh /root/run_sleep.sh
root 2410 0.0 0.0 113120 1196 ? Ss 22:50 0:00 sh /root/run_sleep.sh
root 2434 0.0 0.0 113120 1196 ? Ss 22:51 0:00 sh /root/run_sleep.sh
不,对脚本的每次调用都是独立于其他脚本的,但是您应该考虑到脚本 运行 同时每次都会使数据库过载......建议运行 脚本至少每两分钟一次 (*/2)
就像 Pepo 说话一样。这将打开一个新进程。在某些情况下,这可能是个问题。要解决这个问题,您可以使用 JobQueue,例如 Laravel。或者,您可以创建一个更好的任务列表来执行您的应用程序进程的 cronjob,类似于作业队列,有些类似于 table 或带有要处理的列表 actions/functions 和项目 pending/processing/finished 的状态的文件。
多进程有这个问题,记得在进程之前检查你的数据。
Will the first execution interrupt, when the script is called again
(every minute it is called), even though the first execution hasn't
finished yet?
不,除非您在被调用的脚本中处理这种情况,否则它不会中断。解决该问题的常用方法是使用 locks,或实现 mutual exclusion.
在PHP中实现锁定的方法有很多种,"best"没有。您可以选择最适合您平台上可用后端的一个,或者为不同平台甚至不同用例实施多个储物柜。请注意,您应该确保始终在同一主机上为特定作业使用同一个储物柜!
一些可以用作锁定后端的流行工具:
例子
以下代码实现了抽象储物柜 class 和基于 phpredis 扩展的示例实现。
namespace Acme;
class Factory {
/// @var \Redis
private static $redis;
public static function redis() {
if (!static::$redis) {
try {
static::$redis = new \Redis();
// In practice you should fetch the host from a configuration object.
static::$redis->pconnect('/tmp/redis.sock');
} catch (\Exception $e) {
trigger_error($e->getMessage(), E_USER_WARNING);
return false;
}
}
return static::$redis;
}
/**
* @param mixed $id ID of a job or group of jobs
* @return AbstractLocker
*/
public static function locker($id) {
return new RedisLocker($id);
}
}
abstract class AbstractLocker {
abstract public function __construct($id);
abstract public function lock();
abstract public function unlock();
abstract public function isLocked();
}
class RedisLocker extends AbstractLocker {
/// Key prefix
const PREFIX = 'lock/';
/// @var \Redis
private static $redis;
/// @var string DB item key
private $key;
/// @var int Expiration time in seconds
private $expire = 86400;
/**
* @param mixed $id ID of a job or group of jobs
*/
public function __construct($id) {
if (!static::$redis) {
static::$redis = Factory::redis();
}
$this->key = static::PREFIX . '/' . $id;
}
public function lock() {
$this->_fixDeadlocks();
$r = static::$redis;
// Set the key to the current process ID
// within a transaction (see http://redis.io/topics/transactions).
$r->multi();
$result = $r->setnx($this->key, getmypid());
$r->setTimeout($this->key, $this->expire);
$r->exec();
return (bool) $result;
}
public function unlock() {
$r = static::$redis;
// Delete the key from DB within a transaction.
$r->multi();
$result = $r->delete($this->key);
$r->exec();
return (bool) $result;
}
public function isLocked() {
$this->_fixDeadlocks();
return (bool) static::$redis->exists($this->key);
}
private function _fixDeadlocks() {
$r = static::$redis;
if (!$r->exists($this->key) || (!$pid = $r->get($this->key))) {
return;
}
$running = (bool) posix_kill($pid, 0);
if ($pid && $running) {
// Another process is running normally
return;
}
if (!$running) {
// Process is not running, so the keys must not exist
if ($r->exists($this->key) && $pid == $r->get($this->key)) {
// Deadlock found
$this->unlock();
}
}
}
}
//////////////////////////////////////////////////////////////////
// Usage
$id = 'Bubbles';
$locker = Factory::locker($id);
if ($locker->isLocked()) {
trigger_error("$id job is locked");
exit(1);
}
$locker->lock();
for ($i = 0; $i < 10; ++$i) { echo '. o O '; usleep(1e6); }
echo PHP_EOL;
$locker->unlock();
测试
航站楼A
$ php script.php
. o O . o O . o O . o O . o O . o
航站楼B
$ php script.php
Notice: Bubbles job is locked in /home/ruslan/tmp/script.php on line 121
我有一个关于 cron 作业的问题。
这里简单介绍一下情况:
我每分钟调用一个 php 脚本 (*****) (PHP CLI)。
PHP 脚本发出一个数据库请求,以检查在特定分钟内是否有任何电子邮件要发送,并相应地发送它们。
PHP脚本执行需要1分钟以上(庞大的数据库->要发送大量电子邮件->即使使用多线程也需要时间)
同一 PHP 脚本在下一分钟 (*****) 被调用,而它的第一次执行尚未完成。
第一次执行是否会中断,当再次调用脚本时(每分钟调用一次),即使第一次执行尚未完成?
希望你的情况清楚。
操作系统:显然Linux ...
感谢您的帮助!
不,将产生一个新进程。快速测试表明:
shell> cat /root/run_sleep.sh
#!/bin/bash
sleep 10000
shell> crontab -l
* * * * * sh /root/run_sleep.sh
您可以检查 ps
每分钟创建不同的进程:
shell> ps aux | grep run[_]
root 2316 0.0 0.0 113120 1192 ? Ss 22:46 0:00 sh /root/run_sleep.sh
root 2339 0.0 0.0 113120 1188 ? Ss 22:47 0:00 sh /root/run_sleep.sh
root 2363 0.0 0.0 113120 1192 ? Ss 22:48 0:00 sh /root/run_sleep.sh
root 2388 0.0 0.0 113120 1192 ? Ss 22:49 0:00 sh /root/run_sleep.sh
root 2410 0.0 0.0 113120 1196 ? Ss 22:50 0:00 sh /root/run_sleep.sh
root 2434 0.0 0.0 113120 1196 ? Ss 22:51 0:00 sh /root/run_sleep.sh
不,对脚本的每次调用都是独立于其他脚本的,但是您应该考虑到脚本 运行 同时每次都会使数据库过载......建议运行 脚本至少每两分钟一次 (*/2)
就像 Pepo 说话一样。这将打开一个新进程。在某些情况下,这可能是个问题。要解决这个问题,您可以使用 JobQueue,例如 Laravel。或者,您可以创建一个更好的任务列表来执行您的应用程序进程的 cronjob,类似于作业队列,有些类似于 table 或带有要处理的列表 actions/functions 和项目 pending/processing/finished 的状态的文件。 多进程有这个问题,记得在进程之前检查你的数据。
Will the first execution interrupt, when the script is called again (every minute it is called), even though the first execution hasn't finished yet?
不,除非您在被调用的脚本中处理这种情况,否则它不会中断。解决该问题的常用方法是使用 locks,或实现 mutual exclusion.
在PHP中实现锁定的方法有很多种,"best"没有。您可以选择最适合您平台上可用后端的一个,或者为不同平台甚至不同用例实施多个储物柜。请注意,您应该确保始终在同一主机上为特定作业使用同一个储物柜!
一些可以用作锁定后端的流行工具:
例子
以下代码实现了抽象储物柜 class 和基于 phpredis 扩展的示例实现。
namespace Acme;
class Factory {
/// @var \Redis
private static $redis;
public static function redis() {
if (!static::$redis) {
try {
static::$redis = new \Redis();
// In practice you should fetch the host from a configuration object.
static::$redis->pconnect('/tmp/redis.sock');
} catch (\Exception $e) {
trigger_error($e->getMessage(), E_USER_WARNING);
return false;
}
}
return static::$redis;
}
/**
* @param mixed $id ID of a job or group of jobs
* @return AbstractLocker
*/
public static function locker($id) {
return new RedisLocker($id);
}
}
abstract class AbstractLocker {
abstract public function __construct($id);
abstract public function lock();
abstract public function unlock();
abstract public function isLocked();
}
class RedisLocker extends AbstractLocker {
/// Key prefix
const PREFIX = 'lock/';
/// @var \Redis
private static $redis;
/// @var string DB item key
private $key;
/// @var int Expiration time in seconds
private $expire = 86400;
/**
* @param mixed $id ID of a job or group of jobs
*/
public function __construct($id) {
if (!static::$redis) {
static::$redis = Factory::redis();
}
$this->key = static::PREFIX . '/' . $id;
}
public function lock() {
$this->_fixDeadlocks();
$r = static::$redis;
// Set the key to the current process ID
// within a transaction (see http://redis.io/topics/transactions).
$r->multi();
$result = $r->setnx($this->key, getmypid());
$r->setTimeout($this->key, $this->expire);
$r->exec();
return (bool) $result;
}
public function unlock() {
$r = static::$redis;
// Delete the key from DB within a transaction.
$r->multi();
$result = $r->delete($this->key);
$r->exec();
return (bool) $result;
}
public function isLocked() {
$this->_fixDeadlocks();
return (bool) static::$redis->exists($this->key);
}
private function _fixDeadlocks() {
$r = static::$redis;
if (!$r->exists($this->key) || (!$pid = $r->get($this->key))) {
return;
}
$running = (bool) posix_kill($pid, 0);
if ($pid && $running) {
// Another process is running normally
return;
}
if (!$running) {
// Process is not running, so the keys must not exist
if ($r->exists($this->key) && $pid == $r->get($this->key)) {
// Deadlock found
$this->unlock();
}
}
}
}
//////////////////////////////////////////////////////////////////
// Usage
$id = 'Bubbles';
$locker = Factory::locker($id);
if ($locker->isLocked()) {
trigger_error("$id job is locked");
exit(1);
}
$locker->lock();
for ($i = 0; $i < 10; ++$i) { echo '. o O '; usleep(1e6); }
echo PHP_EOL;
$locker->unlock();
测试
航站楼A
$ php script.php
. o O . o O . o O . o O . o O . o
航站楼B
$ php script.php
Notice: Bubbles job is locked in /home/ruslan/tmp/script.php on line 121