Итак, прежде всего немного фона.
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);
}
}
Интересно отметить:
- Atomi c (или нить- безопасные) операции не нуждаются в такой защите
- Код, который мы помещаем между получением и снятием блокировки, можно считать «преобразованным» в операцию atomi c.
- Получение Сама блокировка должна быть поточно-ориентированной или атомарной c.
На уровне операционной системы мьютексные блокировки обычно реализуются с использованием инструкций процессора atomi c, созданных для этой спецификации * 1071. * цель, такая как атоми c тестирование и установка операция. Это будет проверять, если значение, если установлено, и если оно не установлено, установите его. Это работает как мьютекс, если вы просто говорите, что сама блокировка - это существование значения. Если он существует, блокировка берется, а если нет, вы получаете блокировку, устанавливая значение.
Laravel реализует блокировки аналогичным образом. В нем используется преимущество atomi c природы операций «установить, если еще не установлен», которые предоставляют определенные драйверы кеша, поэтому блокировки работают только тогда, когда есть эти указанные c драйверы кеша.
Однако вот самое важное:
В блокировке с проверкой и установкой сама блокировка - это ключ кеша, проверяемый на существование. Если ключ установлен, то блокировка взята и , как правило, не может быть повторно получена. Обычно блокировки реализуются с помощью «обхода», в котором, если один и тот же процесс пытается получить одну и ту же блокировку несколько раз, это успешно выполняется. Это называется reentrant mutex и позволяет использовать один и тот же объект блокировки во всем критическом разделе, не беспокоясь о своей блокировке. Это полезно, когда критическая секция усложняется и охватывает несколько функций.
Теперь у вас есть два fl aws с вашей логикой c:
- Использование одного и того же ключа и для блокировки, и для значения это то, что разрушает вашу блокировку. В аналогии с замком вы пытаетесь хранить свои ценности в сейфе, который сам является частью ваших ценностей. Это невозможно.
- У вас есть
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
заключается в том, что в случае, если есть за исключением того, что вы все еще хотите, чтобы блокировка была снята, а не оставалась "застрявшей".