如何防止脚本的多个实例?

时间:2009-12-07 16:59:47

标签: php mysql

我有php脚本,我必须在Linux和Windows服务器上运行。 我想使用相同的脚本而不对这两个环境进行任何修改。

这些脚本将使用cron(在linux上)和Windows调度程序(或其他,我现在不关心)安排在我的Windows环境中。

但是,某些脚本可能需要几分钟才能完成。我只想防止调度程序(cron或windows的)在上次启动它之前启动相同的脚本。

我不知道该怎么做.. 我想确保在执行过程中出现问题时释放“锁定”,以便下次再次启动而无需人工干预。

也许在虚拟文件上有一群人可以做到这一点,但我不知道该怎么做。

我在这些服务器上也有一个MySQL数据库。我想可能在数据库端使用锁。

1- Start a transaction
2- Insert script name in a table.
3- execution of the script.
4- If successful then delete the row and commit the transaction or simply rollback;

如果脚本名称在表中,那么我可以阻止它运行。 如果脚本执行失败,那么Mysql将自动回滚事务,以便在下次调用脚本时不显示该行。

但是,在一个事务中,有没有办法让其他连接看到未提交的数据?如果有,怎么样?

如果无法使用回滚的话,我还想过在行上使用锁..

1- Insert script name in a table if it doesn't already exists.
2- Start a transaction.
2- Select * from Table where script_name FOR UPDATE.
3- execution of the script.
4- If successful then release the lock (rollback or commit).

但我的主要问题是Mysql。选择FOR UPDATE挂起,直到释放上一个锁或者超过50秒超时(innodb_lock_wait_timeout变量)为止。我希望Mysql当场告诉我我的行被锁定而不影响整个数据库。这是因为innodb_lock_wait_timeout变量是全局变量(不是会话变量)。 是否有另一个变量模仿Oracle中可用的NO_WAIT子句?

或者我应该让脚本挂起50秒没有任何问题?

最好的方法是什么,因为我是一个php新手,我不想在服务器上造成任何问题。

也许我有另一个选项,我没有看到..

5 个答案:

答案 0 :(得分:10)

我使用...套接字解决了这个问题。你可以启用php_sockets扩展然后尝试它。这是代码示例:

$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if (false === $socket) {
    throw new Exception("can't create socket: ".socket_last_error($socket));
}
## set $port to something like 10000
## hide warning, because error will be checked manually
if (false === @socket_bind($socket, '127.0.0.1', $port)) {
    ## some instanse of the script is running
    return false;
} else {
    ## let's do your job
    return $socket;
}

特定$port上的绑定套接字是concurent执行的安全操作。操作系统将确保没有其他进程将套接字绑定到同一端口。您只需要检查返回值。

如果脚本崩溃,则操作系统将自动取消绑定端口。

这也可以用于任何语言。我已经在perl和基于php的项目上测试了它。即使我们错误地在crontab中添加了两次脚本,它也会停止并行执行。

答案 1 :(得分:2)

为什么不使用老式的信号量,它就是为此而准备的。我确信Windows的实现也可用,或者PHP只是兼容:

if ($theSemaphore = sem_get("123456",1)) { // this "1" ensures that there is nothing parallel
  if (sem_acquire($theSemaphore)) {  // this blocks the execution until other processes or threads are finished
    <put your code to serialize here>
    sem_release($theSemaphore); // This should be called only if sem_acquire() succeeds
  }
}

在Apache线程环境中,这个工作正常,也在PHP-CLI中并且是混合的。如果进程意外死亡,则信号量无效并且再次获取van。 信号量是“原子”实现的,因此可以防止锁定期间的竞争条件。

A nice description based on toilets is here

答案 2 :(得分:1)

检查锁文件(即“script_running.lock”) 伪代码:

if file exists exit
else
create the file
run the rest of the script
unlink the file when script is done

答案 3 :(得分:1)

2017年现代答案:

在PHP中实现锁的方法有很多种。

  • 哑文件存在检查,又名“新手的第一次婴儿锁定尝试”:只需创建一个文件,然后在完成后将其删除。所有其他实例检查它是否存在。这样做会有很大的风险,即文件在完成后不会被删除(例如断电或脚本被强行杀死),这意味着所有以后的脚本运行都会失败。它还会遇到多个同时启动的实例,这些实例看到文件丢失并且所有实例都尝试专门创建它。这是可怕的
  • 将进程ID写入文件:略微改进上述方法。但仍然非常 hacky并且受到竞争条件的影响。您将当前PHP实例的进程ID写入文本文件,然后所有其他实例读取该文件的内容并检查该进程ID是否仍然存在且是否为PHP进程,如果是,我们认为该进程“已锁定” 。如果两个脚本彼此非常接近并且两者读取相同的文本文件内容并且两者认为之前的PHP进程ID不再运行,则它很容易受到竞争条件的影响两者相信他们拥有独家锁。应该以所有成本避免使用此方法。即使对于基本的Bash shell脚本(没有其他方法可用), 几乎可接受,但PHP有更高级的锁定方法。
  • 套接字:绑定到本地端口。如果端口正在使用中,请将其视为“已锁定”。优点:不需要锁定文件。缺点:系统本身可能正在使用该端口,或者系统的配置可能不允许进程执行端口绑定,并且它也比锁定文件慢得多(因为它调用操作系统的整个套接字系统并创建一个套接字)。
  • 信号量:非常快速可靠,但是Unix-only(Posix)。
  • 独家文件锁:这是跨平台的(Unix,Linux,Mac,Windows),超级可靠且快速。这就是我在下面干净利落地实施的。

<强> locker.inc.php:

<?php

class Locker
{
    private $_filename;
    private $_fh = NULL;

    public function __construct( string $filename )
    {
        $this->_filename = $filename;
    }

    public function __destruct()
    {
        $this->unlock();
    }

    /**
     * Attempt to acquire an exclusive lock. Always check the return value!
     * @param bool $block If TRUE, we'll wait for existing lock release.
     * @return bool TRUE if we've acquired the lock, otherwise FALSE.
     */
    public function lock( bool $block = TRUE )
    {
        // Create the lockfile if it doesn't exist.
        if( ! is_file( $this->_filename ) ) {
            $created = @touch( $this->_filename );
            if( ! $created ) {
                return FALSE; // no file
            }
        }

        // Open a file handle if we don't have one.
        if( $this->_fh === NULL ) {
            $fh = @fopen( $this->_filename, 'r' );
            if( $fh !== FALSE ) {
                $this->_fh = $fh;
            } else {
                return FALSE; // no handle
            }
        }

        // Try to acquire the lock (blocking or non-blocking).
        $lockOpts = ( $block ? LOCK_EX : ( LOCK_EX | LOCK_NB ) );
        return flock( $this->_fh, $lockOpts ); // lock
    }

    /**
     * Release the lock. Also happens automatically when the Locker
     * object is destroyed, such as when the script ends. Also note
     * that all locks are released if the PHP process is force-killed.
     * NOTE: We DON'T delete the lockfile afterwards, to prevent
     * a race condition by guaranteeing that all PHP instances lock
     * on the exact same filesystem inode.
     */
    public function unlock()
    {
        if( $this->_fh !== NULL ) {
            flock( $this->_fh, LOCK_UN ); // unlock
            fclose( $this->_fh );
            $this->_fh = NULL;
        }
    }
}

<强> testlock.php:

<?php

require_once( 'locker.inc.php' );

$locker = new Locker( 'test.lock' );

echo time() . ": acquiring lock...\n";
$is_locked = $locker->lock( TRUE ); // TRUE = blocking
if( $is_locked ) { // ALWAYS check this return value
    echo time() . ": we have a lock...\n";
    sleep(10); // hold the lock for 10 seconds
    // manually unlock again, but we don't have
    // to do this since it also happens when
    // the $locker object is destroyed (i.e.
    // when the script ends).
    $locker->unlock();
} else {
    echo time() . ": failed to get lock...\n";
}

如果您不希望其他脚本在队列中等待锁定被释放,则可以在测试脚本中将TRUE更改为FALSE。

所以你的选择是:

  • TRUE:等到锁定可用。如果你想要所有的工作都要运行,那就太好了,但他们都必须等待轮到他们独自运营。
  • FALSE:如果锁不可用,请不要等待。如果要在其实例已经运行的情况下中止其他脚本,则非常有用。

答案 4 :(得分:0)

或者你也可以使用LOCK文件。这个想法很简单:如果执行脚本S,它将首先检查某个(唯一)文件是否存在,比如S.lock

  • 如果文件存在,S将终止。

  • 否则,它会创建它。如果S退出,则文件将被删除。