我正在尝试更新APC中的变量,将有许多进程尝试进行此操作。
APC不提供锁定功能,因此我正在考虑使用其他机制...... 我目前发现的是mysql的GET_LOCK()和php的flock()。还有其他值得考虑的吗?
更新:我找到了sem_acquire,但它似乎是一种阻塞锁。
我正在尝试更新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
}
};
flock
会被清除,而当PHP进程退出时,文件也会被关闭。因此不应该存在问题。 - Samflock
并锁定磁盘上的文件比实际的APC操作要昂贵得多,因此是一种非常昂贵的协调APC缓存的方式。 - erik258fwrite(...)
可能应该在您解锁该文件之前完成。 - Alexis Wilkeapc_add
仅在变量未存储时成功;因此提供了一种锁定机制。TTL可用于确保失败的锁持有者不会永远持有锁定。apc_add
是正确的解决方案,是因为它避免了检查锁定并将其设置为“由您锁定”之间可能存在的竞态条件。由于apc_add
仅在变量未设置时设置值(“添加”到缓存中),因此确保即使两次调用在时间上非常接近,也不能同时获得锁定。任何不同时检查和设置锁定的解决方案都将固有地受到这种竞争条件的影响;因此需要一次原子操作才能成功进行锁定而避免竞态条件。Memcache
还提供了原子add函数,因此也可以与此技术一起使用,这是一种在主机之间进行锁定的方法之一。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);
}
如果缓存有效,就直接使用。如果缓存为空,则需要获取锁。一旦获得锁,您需要再次检查缓存,以确保在等待获取锁时,缓存没有重新填充。如果缓存已经重新填充,请使用该值并释放锁定,否则进行计算,填充缓存,然后释放锁定。
实际上,检查一下这个方法是否比Peter的建议更好。
使用独占锁,如果您对此感到舒适,请将尝试锁定文件的所有其他内容放入2-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;
}
APC现在被认为是未维护和已死。它的后继者APCu通过apcu_entry
提供锁定。但请注意,它还禁止并发执行任何其他APCu函数。根据您的用例,这可能对您没问题。
来自手册:
注意:当控制进入
apcu_entry()
时,缓存的锁定是独占的,当控制离开apcu_entry()
时,它会被释放:实际上,这将使generator
的主体成为一个临界区,不允许两个进程同时执行相同的代码路径。此外,它还禁止并发执行任何其他APCu函数,因为它们将获取相同的锁。
我知道这个问题已经有一年了,但是我在研究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");
}
function key_for_lock($str) {
return md5($str."locked");
}
$expire
参数是 APC 的一个好特性,它可以防止因脚本死亡或其他类似情况导致锁永久存在。
希望这个答案对于一年后来到这里的任何人都有所帮助。
aquire_lock
不是原子操作,因此当需要锁定某些资源以进行并发访问时,它并不是真正有用的。 - cweiske不能说这是处理工作的最佳方式,但至少它很方便。
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());
});
如果您在同一文件中多次使用此函数,则第三个可选参数可能会很方便。
最后但并非最不重要的是,修改此函数(同时保持其签名)以在以后使用任何其他类型的锁定机制并不困难,例如,如果您发现自己正在使用多个服务器。
自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']);
}