【问题标题】:How to prevent multiples instances of a script?如何防止脚本的多个实例?
【发布时间】:2010-12-24 02:19:05
【问题描述】:

我有 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 新手,我不想在服务器上造成任何问题。

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

【问题讨论】:

  • 您的脚本是否使用了 MySQL 数据库?还是其他一些资源?
  • 有些可以,有些不可以...这些脚本是可配置的,我想在所有这些脚本将继承的基类上实现锁定。所以使用数据库是可能的。

标签: php mysql


【解决方案1】:

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

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

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

【讨论】:

    【解决方案2】:

    2017 年现代答案:

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

    • 哑文件存在检查,又名“新手的第一次婴儿锁尝试”:只需创建一个文件,然后在完成后将其删除。所有其他实例检查它是否存在。这会带来执行完成后文件未被删除的巨大风险(例如断电或强制终止脚本),这意味着 所有 未来的脚本运行都会失败。它还受到多个同时启动的实例的影响,这些实例看到文件丢失并且都试图以独占方式创建它。这可怕
    • 将进程 ID 写入文件:对上述方法稍作改进。但仍然非常 hacky 并且受到竞争条件的影响。您将当前 PHP 实例的进程 ID 写入文本文件,然后所有其他实例读取该文件的内容并检查该进程 ID 是否仍然存在并且是 PHP 进程,如果是,我们认为该进程“已锁定” .如果两个脚本开始时彼此非常接近并且 both 读取相同的文本文件内容并且 both 认为之前的 PHP 进程 ID 不再运行并且双方都相信他们有一个排他锁。应该不惜一切地避免这种方法。即使对于基本的 Bash shell 脚本(没有其他方法可用),它几乎也可以接受,但是 PHP 有更高级的可用的锁定方法。
    • 套接字:绑定到本地端口。如果端口正在使用中,请将其视为“锁定”。优点:不需要锁定文件。缺点:端口可能被系统自己占用了,或者系统的配置可能不允许进程进行端口绑定,而且也比锁定文件慢很多(因为它调用了操作系统的整个socket系统并创建了一个socket )。
    • 信号量:非常快速和可靠,但 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:如果锁不可用,不要等待。如果您想在某个实例已在运行时中止其他脚本,这很有用。

    【讨论】:

      【解决方案3】:

      为什么不使用老式的信号量,它就是为此而生的。我确信也有适用于 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

      【讨论】:

        【解决方案4】:

        我使用...套接字解决了这个问题。您可以启用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 上绑定套接字是并发执行的安全操作。操作系统将确保没有其他进程将套接字绑定到同一端口。你只需要检查返回值。

        如果脚本崩溃,操作系统会自动解除绑定端口。

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

        【讨论】:

        • 确实不错,而且很简单。谢谢
        【解决方案5】:

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

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

        【讨论】:

        • 如果脚本崩溃,这不会也失败吗? IE。 file 永远不会得到 unlinked
        • 您可以设置一个关闭处理程序/错误处理程序进行清理。不过,上面的套接字选项更好:)
        • 这仍然不能解决这种情况,因为您不能保证处理程序正在运行(崩溃的程序不会运行处理程序)。您应该打开文件,如果它尚不存在则创建它并非阻塞锁定它(flock 或类似)。如果锁定失败,那么您的程序已经在运行。如果您的程序崩溃,操作系统会释放锁定。如果操作系统在重新启动时崩溃,则文件不会被锁定。您保持文件打开并锁定,直到您的脚本退出。
        猜你喜欢
        • 2014-10-30
        • 1970-01-01
        • 2016-11-29
        • 1970-01-01
        • 2016-06-25
        • 2014-07-10
        • 1970-01-01
        • 2011-01-11
        • 1970-01-01
        相关资源
        最近更新 更多