我的框架是Laravel 7,缓存驱动程序是Memcached。我想执行原子缓存get/编辑/put。为此,我使用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);
}
发布于 2020-05-01 06:26:30
所以首先是一些背景。
正如您正确提到的那样,互斥(互斥)锁的目的是通过确保只有一个线程或进程进入临界截面来防止争用条件。
但是首先,什么是关键部分?
请考虑以下代码:
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;
}
这里的问题是,如果两个进程同时运行此函数,它们都将在大约同一时间进入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);
}
}
值得指出的有趣之处是:
在操作系统级别,互斥锁通常使用为此特定目的构建的原子处理器指令来实现,例如原子测试集操作。这将检查是否设置了一个值,如果没有设置,则将其设置。如果您只是说锁本身就是值的存在,那么它作为互斥体工作。如果存在锁,则采取锁,如果锁不存在,则通过设置值来获取锁。
Laravel以类似的方式实现了这些锁。它利用了某些缓存驱动程序提供的"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();
}
将$lock->release()
放在finally
部件中的原因是,如果出现异常,您仍然希望释放锁,而不是“卡住”。
另外要注意的是,由于PHP的性质,您还需要设置锁在自动释放之前保持的持续时间。这是因为在某些情况下(例如,当PHP耗尽内存时),进程会突然终止,因此无法运行任何清理代码。锁的持续时间可以确保即使在这种情况下也能释放锁,并且应该将锁的持续时间设置为锁合理持有的最大绝对时间。
发布于 2020-04-28 15:46:28
Cache::lock('post_' . $post_id, 10)->get()
返回false,因为'post_' . $post_id
被锁定了,所以锁还没有释放。
所以你需要先释放锁:
Cache::lock('post_' . $post_id)->release()
// or release a lock without respecting its current owner
Cache::lock('post_' . $post_id)->forceRelease();
然后再试一次,它将返回true
。
并建议使用try catch
或block
设置指定的时限,Laravel将等待此时限。将抛出一个Illuminate\Contracts\Cache\LockTimeoutException
,可以释放锁。
use Illuminate\Contracts\Cache\LockTimeoutException;
$lock = Cache::lock('post_' . $post_id, 10);
try {
$lock->block(5);
...
Cache::put('post_' . $post_id, $post_data, 5 * 60);
$lock->release();
// Lock acquired after waiting maximum of 5 seconds...
} catch (LockTimeoutException $e) {
// Unable to acquire lock...
} finally {
optional($lock)->release();
}
Cache::lock('post_' . $post_id, 10)->block(5, function () use ($post_id, $post_data) {
// Lock acquired after waiting maximum of 5 seconds...
...
Cache::put('post_' . $post_id, $post_data, 5 * 60);
$lock->release();
});
发布于 2022-05-12 16:39:45
在我的例子中,我的Redis配置导致了使Cache:lock
总是返回false的问题。这是因为我在配置文件上重命名命令DEL
和FLUSHDB
,这是Laravel用来释放锁的。
我认为重命名命令会提高安全性,但它会在应用程序级别造成问题。因此,如果有人使用Redis作为驱动程序,那么不要重命名DEL
和FLUSHDB
。我需要一个小时才能弄清楚,希望它能帮助到其他人。
Debian
中类似于/etc/redis/redis.conf
的配置文件
rename-command FLUSHDB ""
rename-command DEL ""
https://stackoverflow.com/questions/61430799
复制相似问题