Почему Cache :: lock () возвращает false в Laravel 7? - PullRequest
3 голосов
/ 25 апреля 2020

Мой фреймворк Laravel 7, а драйвер кэша - Memcached. Я хочу выполнить кеширование атома c получить / редактировать / поставить. Для этого я использую Cache::lock(), но, похоже, это не работает. $lock->get() возвращает false (см. Ниже). Как я могу решить эту проблему?

Тестирование форта, я перезагружаю 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);
}

Ответы [ 2 ]

2 голосов
/ 01 мая 2020

Итак, прежде всего немного фона.

A блокировка взаимного исключения (мьютекс) , как вы правильно упомянули, предназначена для предотвращения условий гонки, гарантируя, что только один поток или процесс когда-либо входит критическая секция .

Но прежде всего, что такое критический раздел?

Рассмотрим этот код:

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 примерно в одно и то же время, и оба преуспеют в выводе средств, однако это может привести к тому, что у пользователя будет отрицательное сальдо, или деньги будут сняты дважды без обновления сальдо (в зависимости от степени несоответствия процессов).

Проблема в том, что операция выполняется в несколько этапов и может быть прервана на любом данном этапе. Другими словами операция НЕ является атомной c.

Это своего рода проблема критической секции, которую решает блокировка взаимного исключения. Вы можете изменить вышеперечисленное, чтобы сделать его более безопасным:

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

}

Интересно отметить:

  1. Atomi c (или нить- безопасные) операции не нуждаются в такой защите
  2. Код, который мы помещаем между получением и снятием блокировки, можно считать «преобразованным» в операцию atomi c.
  3. Получение Сама блокировка должна быть поточно-ориентированной или атомарной c.

На уровне операционной системы мьютексные блокировки обычно реализуются с использованием инструкций процессора atomi c, созданных для этой спецификации * 1071. * цель, такая как атоми c тестирование и установка операция. Это будет проверять, если значение, если установлено, и если оно не установлено, установите его. Это работает как мьютекс, если вы просто говорите, что сама блокировка - это существование значения. Если он существует, блокировка берется, а если нет, вы получаете блокировку, устанавливая значение.

Laravel реализует блокировки аналогичным образом. В нем используется преимущество atomi c природы операций «установить, если еще не установлен», которые предоставляют определенные драйверы кеша, поэтому блокировки работают только тогда, когда есть эти указанные c драйверы кеша.

Однако вот самое важное:

В блокировке с проверкой и установкой сама блокировка - это ключ кеша, проверяемый на существование. Если ключ установлен, то блокировка взята и , как правило, не может быть повторно получена. Обычно блокировки реализуются с помощью «обхода», в котором, если один и тот же процесс пытается получить одну и ту же блокировку несколько раз, это успешно выполняется. Это называется reentrant mutex и позволяет использовать один и тот же объект блокировки во всем критическом разделе, не беспокоясь о своей блокировке. Это полезно, когда критическая секция усложняется и охватывает несколько функций.

Теперь у вас есть два fl aws с вашей логикой c:

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

Причина наличия $lock->release() в части finally заключается в том, что в случае, если есть за исключением того, что вы все еще хотите, чтобы блокировка была снята, а не оставалась "застрявшей".

1 голос
/ 28 апреля 2020

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();
});
...