Альтернативы транзакциям
Задача
Вам нужно выполнить транзакцию, но ваш сервер MySQL не поддерживает их.
Решение
Некоторые транзакционные операции можно заменить явной блокировкой таблицы. А в некоторых случаях транзакция вообще не нужна, можно переформулировать запрос так, что необходимость в транзакции полностью отпадет.
Обсуждение
Транзакции очень полезны, но в некоторых ситуациях их невозможно или не нужно использовать:
• Ваш сервер может вообще не поддерживать транзакции. (Он может быть слишком старым или у него могут быть не установлены соответствующие типы таблиц, см. рецепт 15.1). В этом случае остается только придумать какую-то замену транзакциям. Для предотвращения проблем с параллельными операциями можно использовать явную блокировку таблиц.
• Иногда приложения используют транзакции там, где в этом нет необходимости. Можно переформулировать предложения и избавиться от транзакций. В результате может даже улучшиться производительность приложения.
Группировка предложений с помощью блокировок
Если ваш сервер не поддерживает транзакции, а вам требуется выполнить группу предложений, не пересекаясь с другими клиентскими приложениями, используйте LOCK TABLE и UNLOCK TABLE:
• Установите для всех используемых таблиц блокировку при помощи LOCK TABLE. (Применяйте блокировку записи для таблиц, которые вы будете изменять, и блокировку чтения для остальных таблиц.) Тогда другие клиенты не смогут изменять таблицы, пока вы будете их использовать.
• Запустите запросы, которые должны выполниться как группа.
• Снимите блокировку при помощи UNLOCK TABLE. Другие клиенты получат доступ к таблицам.
Блокировки, установленные LOCK TABLE, действуют, пока вы их не снимете, в том числе и на период выполнения нескольких предложений. То есть обеспечивается решение проблемы конкуренции, как и в транзакции. Однако нет отката на случай ошибки, поэтому блокировка таблиц подходит не для каждого приложения. Например, вы можете попытаться выполнить операцию передачи денег от Евы Иде так:
LOCK TABLE money WRITE;
UPDATE money SET amt = amt - 6 WHERE name = 'Eve';
UPDATE money SET amt = amt + 6 WHERE name = 'Ida';
UNLOCK TABLE;
К сожалению, если второе обновление не выполнится, откатить первое будет невозможно. Но несмотря на некоторые ограничения, есть ряд ситуаций, для которых вполне подходят блокировки таблиц:
• Набор предложений, состоящий только из запросов SELECT. Если вы хотите выполнить несколько предложений SELECT и запретить другим клиентам изменять таблицы, пока вы к ним обращаетесь, используйте блокировку. Например, если вам нужно выполнить несколько суммарных запросов для множества таблиц, может оказаться, что итоги формируются для различных наборов данных, если между запросами другие клиенты будут иметь возможность изменять записи. Итоговые данные получатся противоречивыми. Чтобы не допустить этого, заблокируйте таблицы на время использования.
• Блокировку можно использовать для набора предложений, в котором только последнее является обновлением. Тогда предыдущие предложения не вносят никаких изменений в таблицы, и если обновление не выполнится, нет необходимости в откате более ранних предложений.
Переформулируем запросы, чтобы не использовать транзакции
Иногда приложения используют транзакции без необходимости. Предположим, что у вас есть таблица meeting, в которой содержится информация о собраниях и заседаниях (включая количество оставшихся билетов), и вы пишете приложение на Perl, содержащее функцию get_ticket(), которая распределяет билеты. Одним из способов реализации функции является проверка счетчика билетов, его уменьшение, если он положительный, и возвращение статуса, показывающего доступность билета. Чтобы воспрепятствовать попыткам нескольких клиентов одновременно завладеть последним билетиком, будем выдавать запросы в транзакции:
sub get_ticket
{
my ($dbh, $meeting_id) = @_;
my $ref = transact_init ($dbh);
my $count = 0;
eval
{
# проверяем текущий счетчик билетов
$count = $dbh->selectrow_array (
"SELECT tix_left FROM meeting
WHERE meeting_id = ?", undef, $meeting_id);
# если билеты остались, уменьшаем счетчик
if ($count > 0)
{
$dbh->do ("UPDATE meeting SET tix_left = tix_left-1
WHERE meeting_id = ?", undef, $meeting_id);
}
$dbh->commit ();
};
$count = 0 if $@; # если произошла ошибка, то билетов больше нет
transact_finish ($dbh, $ref, $@);
return ($count > 0)
}
Функция корректно распределяет билеты, но выполняет ненужную работу. Можно сделать то же самое, не прибегая к транзакции. Будем уменьшать счетчик билетов, только если он положительный, затем проверять, изменило ли предложение строку:
sub get_ticket
{
my ($dbh, $meeting_id) = @_;
my $count = $dbh->do ("UPDATE meeting SET tix_left = tix_left-1
WHERE meeting_id = ? AND tix_left > 0",
undef, $meeting_id);
return ($count > 0);
}
В MySQL счетчик строк, возвращаемый предложением UPDATE, указывает на количество измененных строк. То есть если билетов на мероприятие не осталось, то UPDATE не изменит строки, и счетчик будет равен нулю. Легче определить, доступен ли билет, при помощи одного запроса, чем выполнять несколько запросов, используя транзакционный подход. Мораль в том, что несмотря на всю важность и полезность транзакций, может оказаться, что в них нет необходимости, и, избавившись от них, вы повысите производительность своего приложения. (Решение, состоящее из одного запроса, – это пример того, что справочное руководство по MySQL называет «атомарной операцией». В руководстве такие операции рассматриваются как эффективная альтернатива транзакциям.)