【问题标题】:best way to obtain a lock in php在php中获得锁定的最佳方法
【发布时间】:2023-03-24 10:09:02
【问题描述】:

我正在尝试更新 APC 中的变量,并且会有很多进程尝试这样做。

APC 不提供锁定功能,所以我正在考虑使用其他机制……目前我发现的是mysql 的GET_LOCK() 和php 的flock()。还有什么值得考虑的吗?

更新:我找到了 sem_acquire,但它似乎是一个阻塞锁。

【问题讨论】:

  • 变量包含什么,确切地说;你为什么担心锁定?您或许可以解决该问题。
  • 一个(迟到的)警告词:MySQL GET_LOCK() 有一个非常危险的行为。第二个 GET_LOCK() 悄悄地释放同一连接上的前一个锁。 MySQL 每个连接只能持有一个锁。使用股票 MySQL 是不可能使用嵌套锁的。它不应该用于通用锁定。
  • 在 MySQL 5.7 中,get_lock 已经解决了上面提到的问题,所以你可以像现在期望的那样使用它:dev.mysql.com/doc/refman/5.7/en/locking-functions.html

标签: php locking apc


【解决方案1】:
/*
CLASS ExclusiveLock
Description
==================================================================
This is a pseudo implementation of mutex since php does not have
any thread synchronization objects
This class uses flock() as a base to provide locking functionality.
Lock will be released in following cases
1 - user calls unlock
2 - when this lock object gets deleted
3 - when request or script ends
==================================================================
Usage:

//get the lock
$lock = new ExclusiveLock( "mylock" );

//lock
if( $lock->lock( ) == FALSE )
    error("Locking failed");
//--
//Do your work here
//--

//unlock
$lock->unlock();
===================================================================
*/
class ExclusiveLock
{
    protected $key   = null;  //user given value
    protected $file  = null;  //resource to lock
    protected $own   = FALSE; //have we locked resource

    function __construct( $key ) 
    {
        $this->key = $key;
        //create a new resource or get exisitng with same key
        $this->file = fopen("$key.lockfile", 'w+');
    }


    function __destruct() 
    {
        if( $this->own == TRUE )
            $this->unlock( );
    }


    function lock( ) 
    {
        if( !flock($this->file, LOCK_EX | LOCK_NB)) 
        { //failed
            $key = $this->key;
            error_log("ExclusiveLock::acquire_lock FAILED to acquire lock [$key]");
            return FALSE;
        }
        ftruncate($this->file, 0); // truncate file
        //write something to just help debugging
        fwrite( $this->file, "Locked\n");
        fflush( $this->file );

        $this->own = TRUE;
        return TRUE; // success
    }


    function unlock( ) 
    {
        $key = $this->key;
        if( $this->own == TRUE ) 
        {
            if( !flock($this->file, LOCK_UN) )
            { //failed
                error_log("ExclusiveLock::lock FAILED to release lock [$key]");
                return FALSE;
            }
            ftruncate($this->file, 0); // truncate file
            //write something to just help debugging
            fwrite( $this->file, "Unlocked\n");
            fflush( $this->file );
            $this->own = FALSE;
        }
        else
        {
            error_log("ExclusiveLock::unlock called on [$key] but its not acquired by caller");
        }
        return TRUE; // success
    }
};

【讨论】:

  • __destruct 不会因为致命错误而被调用吗?我担心可能导致锁永久卡住并需要人工干预的情况
  • flocks 在文件关闭时被清除,在 php 进程退出时文件被关闭。所以应该不是问题。
  • 调用flock 并锁定磁盘上的文件比实际的 APC 操作要昂贵得多,因此是一种非常昂贵的协调 APC 缓存的方法。
  • @JohnSmith,我做了一些编辑。现在应该可以正常工作了。
  • 请注意,“解锁”的fwrite(...) 可能应该在您解锁所述文件之前完成。
【解决方案2】:

您可以使用apc_add 函数来实现此目的,而无需借助文件系统或mysql。 apc_add 仅在变量尚未存储时成功;因此,提供了一种锁定机制。 TTL 可用于确保失败的锁持有者不会永远持有锁。

apc_add 是正确解决方案的原因是因为它避免了在检查锁和将其设置为“由您锁定”之间存在的竞争条件。由于apc_add 仅在它没有已经 设置的情况下设置值(“添加”到缓存中),它确保锁不能被两个调用同时获取,无论它们是否接近及时。不检查同时设置锁的任何解决方案都不会固有地受到这种竞争条件的影响;需要一个原子操作才能在没有竞争条件的情况下成功锁定。

由于 APC 锁只存在于 php 执行的上下文中,它可能不是一般锁定的最佳解决方案,因为它不支持主机之间的锁。 Memcache 还提供了原子添加功能,因此也可以与这种技术一起使用——这是在主机之间锁定的一种方法。 Redis 还支持原子 'SETNX' 函数和 TTL,是一种非常常用的主机间锁定和同步方法。但是,OP 特别要求为 APC 提供解决方案。

【讨论】:

  • 如果创建原始变量的进程在没有删除它的情况下死亡,如何释放“锁”?我猜它不会。在发生故障时自动释放锁是锁的一个重要特性。
  • 好问题@Jason,我会扩展答案。
【解决方案3】:

如果锁定的目的是防止多个进程尝试填充一个空的缓存键,为什么不想要一个阻塞锁呢?


  $value = apc_fetch($KEY);

  if ($value === FALSE) {
      shm_acquire($SEMAPHORE);

      $recheck_value = apc_fetch($KEY);
      if ($recheck_value !== FALSE) {
        $new_value = expensive_operation();
        apc_store($KEY, $new_value);
        $value = $new_value;
      } else {
        $value = $recheck_value;
      }

      shm_release($SEMAPHORE);
   }

如果缓存是好的,你只需滚动它。如果缓存中没有任何内容,您将获得锁。获得锁后,您需要仔细检查缓存,以确保在等待获得锁时,缓存没有被重新填充。如果缓存被重新填充,则使用该值并释放锁,否则,您进行计算,填充缓存然后释放锁。

【讨论】:

  • 不使用阻塞锁的原因是因为这些进程的负载会大大降低。我宁愿他们不要更新变量,也不愿等待它们积累并导致崩溃。
【解决方案4】:

实际上,看看这是否会比彼得的建议更好。

http://us2.php.net/flock

使用独占锁,如果您觉得舒服,请将所有其他尝试锁定文件的内容置于 2-3 秒睡眠状态。如果处理得当,您的网站将遇到锁定资源的挂起,但不会有一大群脚本争先恐后地缓存相同的内容。

【讨论】:

    【解决方案5】:

    如果您不介意将锁定基于文件系统,那么您可以将 fopen() 与模式“x”一起使用。这是一个例子:

    $f = fopen("lockFile.txt", 'x');
    if($f) {
        $me = getmypid();
        $now = date('Y-m-d H:i:s');
        fwrite($f, "Locked by $me at $now\n");
        fclose($f);
        doStuffInLock();
        unlink("lockFile.txt"); // unlock        
    }
    else {
        echo "File is locked: " . file_get_contents("lockFile.txt");
        exit;
    }
    

    见 www.php.net/fopen

    【讨论】:

    • 只要您从不需要 NFS,这可能是最简单的解决方案。尽管如果锁定脚本在释放羊群之前崩溃,则很有可能会出现竞争条件或更糟的情况。
    • 是的,如果脚本崩溃,您可能会遇到一堆问题,但有一些方法可以解决这个问题,或者至少使用锁定文件中写入的 PID 和时间检测问题并发送电子邮件.
    【解决方案6】:

    我意识到这已经有一年了,但我只是在自己做一些关于锁定 PHP 的研究时偶然发现了这个问题。

    我突然想到,使用 APC 本身可能有一个解决方案。说我疯了,但这可能是一种可行的方法:

    function acquire_lock($key, $expire=60) {
        if (is_locked($key)) {
            return null;
        }
        return apc_store($key, true, $expire);
    }
    
    function release_lock($key) {
        if (!is_locked($key)) {
            return null;
        }
        return apc_delete($key);
    }
    
    function is_locked($key) {
        return apc_fetch($key);
    }
    
    // example use
    if (acquire_lock("foo")) {
        do_something_that_requires_a_lock();
        release_lock("foo");
    }
    

    实际上,我可能会在其中添加另一个函数来生成要在此处使用的密钥,以防止与现有 APC 密钥发生冲突,例如:

    function key_for_lock($str) {
        return md5($str."locked");
    }
    

    $expire 参数是 APC 的一个很好用的特性,因为它可以防止你的锁在你的脚本死掉或类似的情况下永远被持有。

    希望这个答案对一年后在这里绊倒的其他人有所帮助。

    【讨论】:

    • 由于aquire_lock 不是原子的,当您因为并发访问某些资源而需要锁定时,它并不是很有用。
    • 如果脚本死了,APC 会释放它吗?
    • acquire_lock 介绍了一个典型的竞争条件。改为使用 apc_add 在一次调用中创建和检查锁。
    【解决方案7】:

    EAccelerator 有它的方法; eaccelerator_lockeaccelerator_unlock

    【讨论】:

      【解决方案8】:

      不能说这是否是处理这项工作的最佳方式,但至少它很方便。

      function WhileLocked($pathname, callable $function, $proj = ' ')
      {
          // create a semaphore for a given pathname and optional project id
          $semaphore = sem_get(ftok($pathname, $proj)); // see ftok for details
          sem_acquire($semaphore);
          try {
              // capture result
              $result = call_user_func($function);
          } catch (Exception $e) {
              // release lock and pass on all errors
              sem_release($semaphore);
              throw $e;
          }
      
          // also release lock if all is good
          sem_release($semaphore);
          return $result;
      }
      

      使用就这么简单。

      $result = WhileLocked(__FILE__, function () use ($that) {
          $this->doSomethingNonsimultaneously($that->getFoo());
      });
      

      如果您在每个文件中多次使用此函数,则第三个可选参数会派上用场。

      最后但并非最不重要的是,修改此函数(同时保留其签名)以在以后使用任何其他类型的锁定机制并不难,例如如果您碰巧发现自己使用多台服务器。

      【讨论】:

        【解决方案9】:

        APC 现在被视为unmaintained and dead。它的继任者APCu 通过apcu_entry 提供锁定。但请注意,它还禁止同时执行任何其他 APCu 功能。根据您的用例,这对您来说可能没问题。

        来自手册:

        注意:当控制进入apcu_entry()时,缓存的锁是独占获得的,当控制离开apcu_entry()时释放它:实际上,这会将generator的主体变成一个临界区,不允许两个进程同时执行相同的代码路径。此外,它禁止任何其他 APCu 函数的并发执行,因为它们将获取相同的锁。

        【讨论】:

          【解决方案10】:

          APCu 从 5.1.0 开始就有 apcu_entry,现在可以用它实现锁机制:

          /** get a lock, will wait until the lock is available,
           * make sure handle deadlock yourself :p
           * 
           * useage : $lock = lock('THE_LOCK_KEY', uniqid(), 50);
           * 
           * @param $lock_key : the lock you want to get it
           * @param $lock_value : the unique value to specify lock owner
           * @param $retry_millis : wait befor retry
           * @return ['lock_key'=>$lock_key, 'lock_value'=>$lock_value]
           */
          function lock($lock_key, $lock_value, $retry_millis) {
              $got_lock = false;
              while (!$got_lock) {
                  $fetched_lock_value = apcu_entry($lock_key, function ($key) use ($lock_value) {
                      return $lock_value;
                  }, 100);
                  $got_lock = ($fetched_lock_value == $lock_value);
                  if (!$got_lock) usleep($retry_millis*1000);
              }
              return ['lock_key'=>$lock_key, 'lock_value'=>$lock_value];
          }
          
          /** release a lock
           * 
           * usage : unlock($lock);
           * 
           * @param $lock : return value of function lock
           */
          function unlock($lock) {
              apcu_delete($lock['lock_key']);
          }
          

          【讨论】:

            【解决方案11】:

            实际上,我发现我根本不需要任何锁定...鉴于我要创建的是所有 class=> 自动加载路径关联的映射,它不需要一个进程是否覆盖另一个进程发现的内容并不重要(如果编码正确,这是极不可能的),因为无论如何数据最终都会到达那里。所以,结果证明是“无锁”。

            【讨论】:

            • 如果是这种情况,您应该关闭问题。
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2010-09-07
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多