为什么 Cache::lock() 在 Laravel 7 中返回 false?

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)

需要指出的有趣的事情是:

  1. 原子(或线程安全)操作不需要这样的保护
  2. 我们放在获取锁和释放锁之间的代码,可以认为已经“转换”为原子操作。
  3. 获取锁本身需要是线程安全的或原子操作。

在操作系统级别,互斥锁通常使用为此特定目的而构建的原子处理器指令来实现,例如原子测试和设置操作。这将检查是否设置了值,如果未设置,则设置它。如果您只是说锁本身就是值的存在,那么这可以作为互斥锁使用。如果存在,则获取锁,如果不存在,则通过设置值获取锁。

Laravel 以类似的方式实现锁。它利用了某些缓存驱动程序提供的“如果尚未设置则设置”操作的原子性质,这就是为什么锁仅在那些特定的缓存驱动程序存在时才起作用的原因。

然而,最重要的是:

在 test-and-set 锁中,锁本身就是被测试是否存在的缓存键。如果设置了密钥,则锁定被获取并且通常不能重新获得。通常,锁是通过“绕过”实现的,如果同一个进程多次尝试获取同一个锁,它就会成功。这称为可重入互斥锁,允许在整个临界区使用相同的锁定对象,而不必担心将自己锁定。当临界区变得复杂并跨越多个功能时,这很有用。

现在你的逻辑有两个缺陷:

  1. 对锁和值使用相同的密钥是破坏你的锁的原因。在锁类比中,您试图将贵重物品存放在保险箱中,而保险箱本身就是贵重物品的一部分。这不可能。
  2. 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 内存不足时),进程会突然终止,因此无法运行任何清理代码。锁的持续时间确保即使在这些情况下也能释放锁,并且持续时间应设置为合理持有锁的绝对最长时间。

  • 哇......这是最好的答案......我真的希望上帝能给你更多的奖励(幸福,更丰富)......谢谢......你真的救了我的命...... (2认同)