在php中获取锁定的最佳方法

时间:2008-11-28 13:50:34

标签: php locking apc

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

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

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

11 个答案:

答案 0 :(得分:15)

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

答案 1 :(得分:8)

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

apc_add是正确解决方案的原因是因为它避免了在检查锁定并将其设置为“由您锁定”之间存在的竞争条件。由于apc_add仅设置设置(将其“添加”到缓存中),因此它仅设置值,因此它确保一次无法通过两次调用获取锁定,而不管他们的时间接近。没有检查同时设置锁定的解决方案本身就会受到这种竞争条件的影响;在没有竞争条件的情况下成功锁定需要一个原子操作。

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

答案 2 :(得分: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);
   }

如果缓存良好,您只需使用它。如果缓存中没有任何内容,则会获得锁定。一旦你有锁,你需要仔细检查缓存,以确保在等待锁定时,缓存没有重新填充。如果重新填充缓存,请使用该值&释放锁定,否则,你进行计算,填充缓存和放大器。然后释放你的锁。

答案 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

答案 4 :(得分:3)

实际上,检查一下这是否会比彼得的建议更好。

http://us2.php.net/flock

使用独占锁,如果您对它感到满意,请将其他所有试图锁定文件的内容放入2-3秒的睡眠状态。如果做得好,你的网站将会遇到关于锁定资源的问题,但不会有大量的脚本正在努力缓存相同内容。

答案 5 :(得分: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使用的一个很好的功能,因为它可以防止你的锁被永久保留,如果你的脚本死了或类似的东西。

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

答案 6 :(得分:0)

我发现,实际上,我根本不需要任何锁定...鉴于我正在尝试创建的是所有类的地图=>自动加载的路径关联,如果一个进程覆盖了另一个进程发现的内容(如果编码正确则极不可能),这无关紧要,因为无论如何数据最终会到达那里。所以,解决方案原来是“无锁”。

答案 7 :(得分:0)

EAccelerator有方法; eaccelerator_lockeaccelerator_unlock

答案 8 :(得分: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());
});

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

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

答案 9 :(得分:0)

APC现在被视为unmaintained and dead。它的继任者APCu通过apcu_entry提供锁定。但请注意,它还禁止并发执行任何其他APCu功能。根据您的使用情况,这可能适合您。

从手册:

  

注意:当控件进入apcu_entry()时,将专门获取缓存的锁定,当控件离开apcu_entry()时,它将被释放:实际上,这将转换generator的正文{1}}进入一个关键部分,禁止两个进程同时执行相同的代码路径。此外,它禁止并发执行任何其他APCu功能,因为它们将获得相同的锁定。

答案 10 :(得分:0)

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']);
}