MySQL: транзакции и таблицы блокировки - PullRequest
98 голосов
/ 19 ноября 2010

Я немного запутался с транзакциями против таблиц блокировок, чтобы обеспечить целостность базы данных и обеспечить синхронизацию 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, чтобы удостовериться, что только 1 соединение делает это одновременно, и разблокировать его, когда я закончу, но это кажется излишним. Обертывание, которое в транзакции сделало бы то же самое(гарантируя, что никакое другое соединение не попытается выполнить тот же процесс, пока другое все еще обрабатывает)? Или SELECT ... FOR UPDATE или SELECT ... LOCK IN SHARE MODE будет лучше?

Ответы [ 6 ]

154 голосов
/ 19 ноября 2010

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

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

$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

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

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

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

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

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

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

14 голосов
/ 19 ноября 2010

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

6 голосов
/ 19 ноября 2010

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

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

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

4 голосов
/ 23 ноября 2015

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

2 голосов
/ 31 августа 2015

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

1 голос
/ 19 ноября 2010

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

START TRANSACTION WITH CONSISTENT SNAPSHOT;

для начала и

COMMIT;

до конца.

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

...