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

110

Я немного запутался в транзакциях и блокировках таблиц, чтобы обеспечить целостность базы данных и убедиться, что 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быть лучше?

Райан
источник

Ответы:

173

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

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

$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

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

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

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

В итоге все сводится к следующему:

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

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

Марк Б
источник
4
Я тоже / все еще в замешательстве. Скажем, в учетной записи получателя для начала было 100 долларов, и мы добавляем оплату счета в размере 20 долларов с нашей учетной записи. Насколько я понимаю, транзакции начинаются так, что любая внутри транзакционная операция видит базу данных в том состоянии, в котором она была в начале транзакции. то есть: пока мы его не изменим, на счету получателя будет 100 долларов. Итак ... когда мы добавляем 20 долларов, мы фактически устанавливаем баланс в 120 долларов. Но что произойдет, если во время нашей транзакции кто-то опустошит счет получателя до 0 долларов? Это как-то предотвращается? Они снова волшебным образом получают 120 долларов? Не поэтому ли нужны и замки?
Russ
Да, здесь в игру вступают замки. Правильная система будет блокировать запись, чтобы никто другой не мог обновлять запись во время выполнения транзакции. Параноидальная система наложила бы на запись безусловную блокировку, чтобы никто не мог прочитать и «устаревший» баланс.
Marc B
1
В основном смотрите на транзакции как на защиту вещей внутри вашего кода. Блокирует защищенные объекты через «параллельные» пути кода. Пока не возникнут тупики ...
Marc B
1
@MarcB, Так почему мы должны делать блокировку явно, если использование только транзакций уже гарантирует, что блокировки на месте? Будет ли даже случай, когда мы должны будем выполнять явную блокировку, потому что одних транзакций недостаточно?
Pacerier
2
Этот ответ неверен и может привести к неверным выводам. Это утверждение: «Блокировки не позволяют никому вмешиваться в какие-либо записи базы данных, с которыми вы имеете дело. Транзакции не позволяют« последующим »ошибкам вмешиваться в« более ранние »действия, которые вы сделали. Ни одно из них само по себе не может гарантировать, что все работает нормально в конец. Но вместе они делают. " - вас уволят, это крайне неправильно и глупо См. статьи: en.wikipedia.org/wiki/ACID , en.wikipedia.org/wiki/Isolation_(database_systems) и dev.mysql.com/doc/refman/5.1/ ru /…
Nikola Svitlica
14

Как вы сказали, вам нужна транзакция 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

Элисон Р.
источник
8

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

Tczhaodachuan
источник
Это должен быть правильный ответ. Блокировка предназначена для предотвращения состояний гонки, а транзакции предназначены для обновления нескольких таблиц с зависимыми данными. Две совершенно разные концепции, несмотря на то, что транзакции используют блокировки.
Голубая вода
6

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

Я нашел решение проблемы здесь: Как писать запросы INSERT IF NOT EXISTS в стандартном SQL

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

Тони
источник
2

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

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

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

START TRANSACTION WITH CONSISTENT SNAPSHOT;

для начала, а

COMMIT;

чтобы закончить.

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

Мартин Шапендонк
источник
1
За исключением того, что таблица, из которой он выбирает, не будет заблокирована для других сеансов, если он специально не заблокирует ее (или пока не произойдет его ОБНОВЛЕНИЕ), что означает, что другие сеансы могут прийти и изменить ее между SELECT и UPDATE.
Элисон Р.
Прочитав «НАЧАТЬ ТРАНЗАКЦИЮ С СОГЛАСОВАННЫМ снимком» в документации MySQL, я не вижу, где он фактически блокирует другое соединение от обновления той же строки. Насколько я понимаю, он увидит, однако, таблицу, запущенную в начале транзакции. Таким образом, если другая транзакция уже выполняется, уже получила строку и собирается ее обновить, вторая транзакция все равно увидит строку до того, как она будет обновлена. Поэтому он потенциально может попытаться обновить ту же строку, что и другая транзакция. Это правильно, или я что-то упускаю в процессе?
Райан
1
@Ryan Это не блокирует; ты прав. Блокировка (или ее отсутствие) определяется типом выполняемых вами операций (SELECT / UPDATE / DELETE).
Элисон Р.
4
Понимаю. Это обеспечивает согласованность чтения вашей собственной транзакции, но не блокирует других пользователей от изменения строки непосредственно перед вами.
Мартин Шапендонк