hor*_*rse 5 laravel laravel-6 laravel-7
我的框架是 Laravel 7,缓存驱动程序是 Memcached。我想执行原子缓存获取/编辑/放置。为此我使用Cache::lock()
但它似乎不起作用。返回$lock->get()
false(见下文)。我该如何解决这个问题?
Fort 测试,我重新加载 Homestead,并仅运行下面的代码。并且锁定永远不会发生。是否有可能Cache::has()
破坏锁定机制?
if (Cache::store('memcached')->has('post_' . $post_id)) {
$lock = Cache::lock('post_' . $post_id, 10);
Log::info('checkpoint 1'); // comes here
if ($lock->get()) {
Log::info('checkpoint 2'); // but not here.
$post_data = Cache::store('memcached')->get('post_' . $post_id);
... // updating $post_data..
Cache::put('post_' . $post_id, $post_data, 5 * 60);
$lock->release();
}
} else {
Cache::store('memcached')->put('post_' . $post_id, $initial, 5 * 60);
}
Run Code Online (Sandbox Code Playgroud)
apo*_*fos 15
首先是一些背景知识。
一个互斥(互斥)锁定为你正确地提到旨在确保只有一个线程或进程来防止竞争条件不断进入临界区。
但首先什么是临界区?
考虑这个代码:
public function withdrawMoney(User $user, $amount) {
if ($user->bankAccount->money >= $amount) {
$user->bankAccount->money = $user->bankAccount->money - $amount;
$user->bankAccount->save();
return true;
}
return false;
}
Run Code Online (Sandbox Code Playgroud)
这里的问题是,如果两个进程同时运行这个函数,它们会几乎同时进入if
支票,并且都成功提款,但是这可能会导致用户余额为负数或资金被双重提款而没有更新余额(取决于过程的异相程度)。
问题是操作需要多个步骤,并且可以在任何给定步骤中断。换句话说,操作不是原子的。
这就是互斥锁解决的临界区问题。您可以修改上述内容以使其更安全:
public function withdrawMoney(User $user, $amount) {
try {
if (acquireLockForUser($user)) {
if ($user->bankAccount->money >= $amount) {
$user->bankAccount->money = $user->bankAccount->money - $amount;
$user->bankAccount->save();
return true;
}
return false;
}
} finally {
releaseLockForUser($user);
}
}
Run Code Online (Sandbox Code Playgroud)
需要指出的有趣的事情是:
在操作系统级别,互斥锁通常使用为此特定目的而构建的原子处理器指令来实现,例如原子测试和设置操作。这将检查是否设置了值,如果未设置,则设置它。如果您只是说锁本身就是值的存在,那么这可以作为互斥锁使用。如果存在,则获取锁,如果不存在,则通过设置值获取锁。
Laravel 以类似的方式实现锁。它利用了某些缓存驱动程序提供的“如果尚未设置则设置”操作的原子性质,这就是为什么锁仅在那些特定的缓存驱动程序存在时才起作用的原因。
然而,最重要的是:
在 test-and-set 锁中,锁本身就是被测试是否存在的缓存键。如果设置了密钥,则锁定被获取并且通常不能重新获得。通常,锁是通过“绕过”实现的,如果同一个进程多次尝试获取同一个锁,它就会成功。这称为可重入互斥锁,允许在整个临界区使用相同的锁定对象,而不必担心将自己锁定。当临界区变得复杂并跨越多个功能时,这很有用。
现在你的逻辑有两个缺陷:
if (Cache::store('memcached')->has('post_' . $post_id)) {
在临界区之外,但它本身应该是临界区的一部分。要解决此问题,您需要为锁使用与用于缓存条目不同的密钥,并将has
检查移到临界区:
$lock = Cache::lock('post_' . $post_id. '_lock', 10);
try {
if ($lock->get()) {
//Critical section starts
Log::info('checkpoint 1'); // if it comes here
if (Cache::store('memcached')->has('post_' . $post_id)) {
Log::info('checkpoint 2'); // it should also come here.
$post_data = Cache::store('memcached')->get('post_' . $post_id);
... // updating $post_data..
Cache::put('post_' . $post_id, $post_data, 5 * 60);
} else {
Cache::store('memcached')->put('post_' . $post_id, $initial, 5 * 60);
}
}
// Critical section ends
} finally {
$lock->release();
}
Run Code Online (Sandbox Code Playgroud)
之所以具有$lock->release()
的finally
部分是因为万一有你仍然希望被释放的锁,而不是停留“卡住”的异常。
另一件要注意的事情是,由于 PHP 的性质,您还需要设置锁在自动释放之前将被持有的持续时间。这是因为在某些情况下(例如 PHP 内存不足时),进程会突然终止,因此无法运行任何清理代码。锁的持续时间确保即使在这些情况下也能释放锁,并且持续时间应设置为合理持有锁的绝对最长时间。