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