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

Posted

技术标签:

【中文标题】为啥 Cache::lock() 在 Laravel 7 中返回 false?【英文标题】:Why does Cache::lock() return false in Laravel 7?为什么 Cache::lock() 在 Laravel 7 中返回 false? 【发布时间】:2020-08-09 08:24:43 【问题描述】:

我的框架是 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);

【问题讨论】:

清除缓存会起作用吗?另外,为什么您使用相同的钥匙锁和存储$post_data @apokryfos 因为是同一个数据。我得到$post_data,更新它并放上去。我希望$post_data 一次只能由一个用户更新(所以我使用锁定)。 如果数据已经设置在缓存中,则无法获取锁,因为通过缓存获取锁本质上是尝试在缓存中设置值。如果该值存在,则假定该锁由其他人持有 @apokryfos 我知道锁定是针对竞争条件的。因此,两个实体想要修改数据,但同时这样做失败了(例如,如果他们想添加一个各自为 1 的计数器,结果可能是 +1,而不是 +2)。正如上面的代码所示,我释放了锁。 @apokryfos 你说我应该使用一个不同的密钥,只保留锁定?锁定后,我会更新 $post_data 或其他任何数据吗? 【参考方案1】:

所以首先介绍一下背景。

您正确提到的mutual exclusion (mutex) 锁定旨在通过确保只有一个线程或进程进入critical section 来防止竞争条件。

但首先什么是临界区?

考虑这段代码:

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);
    


要指出的有趣的事情是:

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

在操作系统级别,互斥锁通常使用为此特定目的构建的原子处理器指令来实现,例如原子test-and-set 操作。这将检查是否设置了值,如果未设置,则设置它。如果你只是说锁本身就是值的存在,这就像一个互斥锁。如果存在,则获取锁,如果不存在,则通过设置值获取锁。

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

但最重要的是:

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

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

    对锁和值使用相同的密钥是破坏您的锁的原因。在锁类比中,您试图将贵重物品存放在保险箱中,而保险箱本身就是您的贵重物品的一部分。这是不可能的。 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();

finally 部分中包含$lock->release() 的原因是,如果出现异常,您仍然希望释放锁而不是“卡住”。

另外需要注意的是,由于 php 的特性,您还需要设置锁定在自动释放之前将保持的持续时间。这是因为在某些情况下(例如,当 PHP 内存不足时)进程会突然终止,因此无法运行任何清理代码。锁的持续时间确保即使在这些情况下也能释放锁,并且持续时间应设置为可以合理持有锁的绝对最大时间。

【讨论】:

哇......这是最好的答案......我真的希望上帝会给你更多的回报(幸福,更丰富)......谢谢......你是真的救了我的命……【参考方案2】:

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 catchblock 设置指定的时间限制,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();
);

【讨论】:

不幸的是,它不起作用。我正在相应地更新问题。问题可能与 Memcached 配置或 Laravel 安装有关吗?

以上是关于为啥 Cache::lock() 在 Laravel 7 中返回 false?的主要内容,如果未能解决你的问题,请参考以下文章

add trandata带来的library cache lock

oracle row cache lock 之sequence

Oracle11g 密码延迟认证导致library cache lock的情况分析

rac数据库默认sql tuning advisor,导致大量library cache lock

重启大法失效?详述Oracle11g因JDBC bug引发异常Library Cache Lock等

记录MySQL因Waiting for query cache lock导致业务宕机的处理