MySQL: транзакции против таблиц блокировки

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

SELECT * FROM table WHERE (...) LIMIT 1

if (condition passes) {
   // Update row I got from the select 
   UPDATE table SET column = "value" WHERE (...)

   ... other logic (including INSERT some data) ...
}

Мне нужно убедиться, что никакие другие запросы не будут мешать и выполнять те же SELECT (чтение "старого значения" до того, как это соединение завершит обновление строки.

Я знаю, что могу по умолчанию использовать LOCK TABLES table, чтобы убедиться, что только одно соединение делает это за раз и разблокирует его, когда я закончил, но это похоже на перебор. Повлияет ли это на транзакцию на одно и то же (если никакое другое соединение не пытается выполнить тот же процесс, пока другой все еще обрабатывает)? Или лучше будет SELECT ... FOR UPDATE или SELECT ... LOCK IN SHARE MODE?

Ответ 1

Блокировка таблиц не позволяет другим пользователям БД влиять на заблокированные строки/таблицы. Но блокировки сами по себе НЕ гарантируют, что ваша логика выйдет в согласованном состоянии.

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

$balance = "GET BALANCE FROM your ACCOUNT";
if ($balance < $amount_being_paid) {
    charge_huge_overdraft_fees();
}
$balance = $balance - $amount_being paid;
UPDATE your ACCOUNT SET BALANCE = $balance;

$balance = "GET BALANCE FROM receiver ACCOUNT"
charge_insane_transaction_fee();
$balance = $balance + $amount_being_paid
UPDATE receiver ACCOUNT SET BALANCE = $balance

Теперь, без блокировок и транзакций, эта система уязвима для различных условий гонки, самой большой из которых является несколько платежей, выполняемых в вашей учетной записи, или учетная запись получателя параллельно. В то время как ваш код имеет ваш баланс, полученный и выполняет функцию vast_overdraft_fees() и еще много чего, вполне возможно, что некоторые другие платежи будут работать с одним и тем же типом кода параллельно. Они будут получать ваш баланс (скажем, 100 долларов США), совершают свои транзакции (вынимают 20 долларов США, которые вы платите, и 30 долларов, за которые вы их заманиваете), и теперь оба пути кода имеют два разных баланса: 80 долларов США и $70. В зависимости от того, какие из них заканчиваются последним, вы получите один из двух балансов в своей учетной записи, вместо 50 долларов США, которые вы должны были получить (от $100 до $20 - $30). В этом случае "банковская ошибка в вашу пользу".

Теперь скажем, вы используете блокировки. Сначала вы получаете платеж по счету (20 долларов США), поэтому он выигрывает и блокирует вашу учетную запись. Теперь у вас есть эксклюзивное использование, и вы можете вычесть 20 долларов из баланса, и снова записать новый баланс... и ваша учетная запись заканчивается на 80 долларов, как ожидается. Но... ухо... Ты пытаешься обновить учетную запись приемника, и он заблокирован и заблокирован дольше, чем позволяет код, выбирая свою транзакцию... Мы имеем дело с глупыми банками, поэтому вместо правильной ошибки обработка кода просто тянет exit(), а ваши 20 долларов исчезают в слое электронов. Теперь у вас 20 долларов, и вы все равно должны 20 долларов за приемник, а ваш телефон получает обратно.

Итак... введите транзакции. Вы начинаете транзакцию, вы дебетеваете свою учетную запись на 20 долларов США, вы пытаетесь кредитовать получателя с помощью 20 долларов... и что-то снова взрывается. Но на этот раз вместо exit() код может просто сделать rollback, а poof, ваши 20 долларов магически добавлены обратно в вашу учетную запись.

В конце концов, это сводится к следующему:

Блокировки не позволяют кому-либо другому вмешиваться в любые записи базы данных, с которыми вы имеете дело. Транзакции содержат любые "более поздние" ошибки от вмешательства в "ранние" вещи, которые вы сделали. Ни один из них не может гарантировать, что в итоге все будет хорошо. Но вместе они делают.

в завтрашнем уроке: Радость тупиков.

Ответ 2

Вы хотите SELECT ... FOR UPDATE или SELECT ... LOCK IN SHARE MODE внутри транзакции, как вы сказали, поскольку обычно SELECT, независимо от того, находятся они в транзакции или нет, не будут блокировать таблицу. Какой из них вы выберете, зависит от того, хотите ли вы, чтобы другие транзакции могли читать эту строку во время выполнения транзакции.

http://dev.mysql.com/doc/refman/5.0/en/innodb-locking-reads.html

START TRANSACTION WITH CONSISTENT SNAPSHOT не будет делать трюк для вас, поскольку другие транзакции все равно могут появиться и изменить эту строку. Это указано прямо в верхней части ссылки ниже.

Если другие сеансы одновременно обновить ту же таблицу [...], вы можете см. таблицу в состоянии, которое никогда не было существует в базе данных.

http://dev.mysql.com/doc/refman/5.0/en/innodb-consistent-read.html

Ответ 3

У меня была аналогичная проблема при попытке IF NOT EXISTS ..., а затем выполнение INSERT, которое вызвало условие гонки, когда несколько потоков обновляли одну и ту же таблицу.

Я нашел решение проблемы здесь: Как писать INSERT, если НЕ СУЩЕСТВУЕТ запросы в стандартном SQL

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

Ответ 4

Вы путаетесь с блокировкой и транзакцией. В RMDB это две разные вещи. Блокировка предотвращает параллельные операции, в то время как транзакция фокусируется на изоляции данных. Проверьте эту отличную статью для разъяснения и изящного решения.

Ответ 5

Концепции транзакций и блокировки различны. Однако транзакция использовала блокировки, чтобы помочь ей следовать принципам ACID. Если вы хотите, чтобы таблица не позволяла другим читать/писать в то же время, пока вы читаете/пишете, для этого вам нужен замок. Если вы хотите убедиться в целостности данных и их консистенции, вам лучше использовать транзакции. Я думаю, что смешанные концепции уровней изоляции в транзакциях с замками. Пожалуйста, найдите уровни изоляции транзакций, SERIALIZE должен быть нужного вам уровня.

Ответ 6

Я бы использовал

START TRANSACTION WITH CONSISTENT SNAPSHOT;

и

COMMIT;

для завершения.

Все, что вы делаете между ними, изолировано от других пользователей вашей базы данных , если ваш механизм хранения поддерживает транзакции (это InnoDB).