在PHP中获得锁的最佳方法

18

我正在尝试更新APC中的变量,将有许多进程尝试进行此操作。

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

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


变量确切包含什么内容?为什么你担心锁定?也许你可以绕过这个问题。 - Rob
1
一个(晚点的)警告:MySQL GET_LOCK() 有非常危险的行为。第二个 GET_LOCK() 静默地释放同一连接上的前一个锁。MySQL 每个连接只能持有一个锁。嵌套锁在标准 MySQL 中是不可能的。它不应该用于通用锁定。 - korkman
1
在MySQL 5.7中,get_lock已经解决了上述问题,因此您现在可以像预期的那样使用它:https://dev.mysql.com/doc/refman/5.7/en/locking-functions.html - juacala
11个回答

18
/*
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
    }
};

3
__destruct在致命错误发生时会被调用吗?我担心可能会出现导致锁定永久卡住并需要手动干预的情况。 - MeatPopsicle
2
当文件关闭时,flock会被清除,而当PHP进程退出时,文件也会被关闭。因此不应该存在问题。 - Sam
调用flock并锁定磁盘上的文件比实际的APC操作要昂贵得多,因此是一种非常昂贵的协调APC缓存的方式。 - erik258
1
@JohnSmith,我已经进行了一些编辑。现在应该可以正常工作了。 - Saran
1
请注意,“Unlocked”的fwrite(...)可能应该在您解锁该文件之前完成。 - Alexis Wilke

10
您可以使用apc_add函数来实现此操作,而无需使用文件系统或mysql。 apc_add仅在变量未存储时成功;因此提供了一种锁定机制。TTL可用于确保失败的锁持有者不会永远持有锁定。
之所以apc_add是正确的解决方案,是因为它避免了检查锁定并将其设置为“由您锁定”之间可能存在的竞态条件。由于apc_add仅在变量未设置时设置值(“添加”到缓存中),因此确保即使两次调用在时间上非常接近,也不能同时获得锁定。任何不同时检查和设置锁定的解决方案都将固有地受到这种竞争条件的影响;因此需要一次原子操作才能成功进行锁定而避免竞态条件。
由于APC锁仅存在于php执行的上下文中,因此它可能不是通用锁定的最佳解决方案,因为它不支持主机之间的锁定。 Memcache还提供了原子add函数,因此也可以与此技术一起使用,这是一种在主机之间进行锁定的方法之一。Redis还支持原子“SETNX”函数和TTL,并且是主机之间锁定和同步的常用方法。然而,OP特别要求使用APC解决方案。

1
如果创建原始变量的进程在不删除它的情况下死亡,那么“锁”如何被释放?我猜它不会。在失败时自动释放锁是锁的重要功能。 - Jason
很好的问题@Jason,我会扩展回答。 - erik258

5

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


  $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);
   }

如果缓存有效,就直接使用。如果缓存为空,则需要获取锁。一旦获得锁,您需要再次检查缓存,以确保在等待获取锁时,缓存没有重新填充。如果缓存已经重新填充,请使用该值并释放锁定,否则进行计算,填充缓存,然后释放锁定。


1
不使用阻塞锁的原因是,由于会有大量的这些进程,它们将显着减慢速度。我宁愿让它们不更新变量,也不要等待并导致积累引起崩溃。 - tpk

3

实际上,检查一下这个方法是否比Peter的建议更好。

http://us2.php.net/flock

使用独占锁,如果您对此感到舒适,请将尝试锁定文件的所有其他内容放入2-3秒的休眠中。如果做得正确,您的网站将遇到锁定资源方面的挂起,但不会有一堆脚本争夺缓存相同的东西。


3

如果您不介意基于文件系统进行锁定,那么可以使用带有模式“x”的fopen()函数。以下是一个示例:

$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,这可能是最简单的解决方案。虽然如果锁定脚本在释放文件锁之前崩溃,就有很大可能出现竞争条件或更糟的情况。 - David
1
是的,如果脚本崩溃,你可能会遇到堆积问题,但是有方法可以解决这个问题,或者至少可以使用锁文件中写入的PID和时间来检测问题并发送电子邮件。 - too much php

1

APC现在被认为是未维护和已死。它的后继者APCu通过apcu_entry提供锁定。但请注意,它还禁止并发执行任何其他APCu函数。根据您的用例,这可能对您没问题。

来自手册:

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


1

我知道这个问题已经有一年了,但是我在研究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 的一个好特性,它可以防止因脚本死亡或其他类似情况导致锁永久存在。

希望这个答案对于一年后来到这里的任何人都有所帮助。


4
由于aquire_lock不是原子操作,因此当需要锁定某些资源以进行并发访问时,它并不是真正有用的。 - cweiske
1
如果脚本死了,APC会释放它吗? - Timo Huovinen
1
"acquire_lock"引入了典型的竞态条件。使用apc_add来创建并在一次调用中检查锁定。 - Konstantin Pelepelin

0

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

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());
});

如果您在同一文件中多次使用此函数,则第三个可选参数可能会很方便。

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


0

EAccelerator 提供了相应的方法;eaccelerator_lockeaccelerator_unlock


0

自5.1.0版本起,APCu拥有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']);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接