我正在尝试更新 APC 中的变量,并且将有许多进程尝试这样做。
APC 不提供锁定功能,所以我正在考虑使用其他机制……我目前发现的是mysql 的GET_LOCK() 和php 的flock()。还有什么值得考虑的吗?
更新:我找到了 sem_acquire,但它似乎是一个阻塞锁。
/*
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
}
};
您可以使用apc_add函数来实现此目的,而无需借助文件系统或 mysql。 apc_add
仅当变量尚未存储时才成功;因此,提供了一种锁定机制。TTL 可用于确保失败的锁持有者不会永远持有锁。
原因apc_add
是正确的解决方案是因为它避免了在检查锁和将其设置为“由您锁定”之间存在的竞争条件。由于apc_add
仅在尚未设置的情况下设置值(将其“添加”到缓存中),因此它确保了两个调用不能同时获取锁,无论它们在时间上是否接近。没有同时检查和设置锁的解决方案天生就会受到这种竞争条件的影响;需要一个原子操作才能在没有竞争条件的情况下成功锁定。
由于 APC 锁只存在于该 php 执行的上下文中,因此它可能不是一般锁定的最佳解决方案,因为它不支持主机之间的锁。 Memcache
还提供了原子添加功能,因此也可以与这种技术一起使用——这是主机之间锁定的一种方法。 Redis
还支持原子 'SETNX' 函数和 TTL,是主机之间锁定和同步的一种非常常用的方法。但是,OP 特别要求为 APC 提供解决方案。
如果锁定的目的是防止多个进程尝试填充一个空的缓存键,那么为什么不想要一个阻塞锁呢?
$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);
}
如果缓存很好,你只需滚动它。如果缓存中没有任何内容,您将获得锁。获得锁后,您需要仔细检查缓存,以确保在等待获得锁时,缓存没有重新填充。如果缓存被重新填充,则使用该值并释放锁,否则,您进行计算,填充缓存然后释放锁。
如果您不介意将锁定基于文件系统,那么您可以将 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
实际上,检查一下这是否会比彼得的建议更好。
使用排他锁,如果你觉得舒服,把试图锁定文件的所有其他东西都放在 2-3 秒的睡眠中。如果操作正确,您的网站将遇到锁定资源的挂起,但不会有大量脚本争夺缓存相同的内容。
我意识到这已经有一年了,但我只是在自己做一些关于锁定 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 的一个很好的功能,因为它可以防止您的锁在您的脚本死亡或类似情况下被永久持有。
希望这个答案对一年后在这里绊倒的其他人有所帮助。
EAccelerator有它的方法;eaccelerator_lock
和eaccelerator_unlock
。
APC 现在被认为是无人维护和死机。它的继任者APCu通过apcu_entry
. 但请注意,它还禁止同时执行任何其他 APCu 功能。根据您的用例,这对您来说可能没问题。
从手册:
注意:当控制进入时
apcu_entry()
,缓存的锁是独占获得的,当控制离开时释放apcu_entry()
:实际上,这会将主体generator
变成临界区,不允许两个进程同时执行相同的代码路径。此外,它禁止任何其他 APCu 函数的并发执行,因为它们将获取相同的锁。
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']);
}
不能说这是否是处理工作的最佳方式,但至少它很方便。
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());
});
如果您在每个文件中多次使用此函数,则第三个可选参数会派上用场。
最后但同样重要的是,修改这个函数(同时保留其签名)以在以后使用任何其他类型的锁定机制并不难,例如,如果您碰巧发现自己在使用多个服务器。
实际上,我发现我根本不需要任何锁定...鉴于我要创建的是所有类 => 用于自动加载的路径关联的映射,如果一个进程会覆盖另一个进程发现的内容(如果编码正确,这是极不可能的),因为无论如何数据最终都会到达那里。因此,解决方案原来是“无锁”。