MySQL и PHP: атомарность и повторяемость блока кода PHP, выполняющего два последующих запроса - насколько опасно?
В MySQL я должен проверить, вернул ли запрос select какие-либо записи, если нет, я вставляю запись. Однако я боюсь, что вся операция if-else в сценариях PHP НЕ настолько атомарна, как хотелось бы, то есть в некоторых сценариях она будет прерываться, например, если вызывается другой экземпляр сценария, где нужно работать с той же записью:
if(select returns at least one record)
{
update record;
}
else
{
insert record;
}
Я не использовал транзакции здесь, и автокоммит включен. Я использую MySQL 5.1 с PHP 5.3. Таблица InnoDB. Я хотел бы знать, является ли приведенный выше код неоптимальным и действительно сломается. Я имею в виду, что один и тот же скрипт повторно вводится двумя экземплярами, и происходит следующая последовательность запросов:
- Экземпляр 1 пытается выбрать запись, не находит ничего, входит в блок для запроса вставки
- Экземпляр 2 пытается выбрать запись, не находит ничего, входит в блок для запроса вставки
- экземпляр 1 пытается вставить запись, успешно
- экземпляр 2 пытается вставить запись, не удается, автоматически завершает работу сценария
Это означает, что экземпляр 2 будет прерван и вернет ошибку, пропуская что-либо после оператора запроса вставки. Я мог бы сделать ошибку не фатальной, но я не люблю игнорировать ошибки, я бы скорее знал, реальны ли мои страхи здесь.
Обновление: что я в итоге делал (это нормально для SO?)
Данная таблица помогает регулировать (действительно разрешать / запрещать) количество сообщений, отправляемых приложением каждому получателю. Система не должна отправлять более X сообщений получателю Y в течение периода Z. Таблица [концептуально] выглядит следующим образом:
create table throttle
(
recipient_id integer unsigned unique not null,
send_count integer unsigned not null default 1,
period_ts timestamp default current_timestamp,
primary key (recipient_id)
) engine=InnoDB;
И блок [несколько упрощенного / концептуального] PHP-кода, который должен выполнять атомарную транзакцию, которая поддерживает правильные данные в таблице и разрешает / запрещает отправку сообщения в зависимости от состояния газа:
function send_message_throttled($recipient_id) /// The 'Y' variable
{
query('begin');
query("select send_count, unix_timestamp(period_ts) from throttle where recipient_id = $recipient_id for update");
$r = query_result_row();
if($r)
{
if(time() >= $r[1] + 60 * 60 * 24) /// The numeric offset is the length of the period, the 'Z' variable
{/// new period
query("update throttle set send_count = 1, period_ts = current_timestamp where recipient_id = $recipient_id");
}
else
{
if($r[0] < 5) /// Amount of messages allowed per period, the 'X' variable
{
query("update throttle set send_count = send_count + 1 where recipient_id = $recipient_id");
}
else
{
trigger_error('Will not send message, throttled down.', E_USER_WARNING);
query('rollback');
return 1;
}
}
}
else
{
query("insert into throttle(recipient_id) values($recipient_id)");
}
if(failed(send_message($recipient_id)))
{
query('rollback');
return 2;
}
query('commit');
}
Что ж, несмотря на то, что возникают InnoDB взаимоблокировки, это довольно хорошо, нет? Я не стучу в грудь или что-то в этом роде, но это просто лучшее сочетание производительности / стабильности, которое я могу сделать, если не считать MyISAM и блокировки всей таблицы, чего я не хочу делать из-за более частых обновлений / вставок по сравнению с выбирает.
2 ответа
Похоже, вы уже знаете ответ на вопрос, и как решить свою проблему. Это реальная проблема, и вы можете использовать одно из следующих для ее решения:
- ВЫБРАТЬ... ДЛЯ ОБНОВЛЕНИЯ
- ВСТАВИТЬ... НА ДУБЛИКАТЬ КЛЮЧЕВОЕ ОБНОВЛЕНИЕ
- транзакции (не используйте MyIsam)
- настольные замки
Это может и может произойти в зависимости от того, как часто выполняется эта страница.
Безопасной ставкой будет использование транзакций. То же самое, что вы написали, все равно будет происходить, за исключением того, что вы можете безопасно проверить на наличие ошибки внутри транзакции (в случае, когда вставка включает в себя несколько запросов и только последние разрывы вставки), позволяя откатить тот, который стал недействительным.
Итак (псевдо):
#start transaction
if (select returns at least one record)
{
update record;
}
else
{
insert record;
}
if (no constraint errors)
{
commit; //ends transaction
}
else
{
rollback; //ends transaction
}
Вы также можете заблокировать стол, но в зависимости от работы, которую вы делаете, вам придется получить эксклюзивную блокировку для всей таблицы (вы не можете SELECT ... FOR UPDATE
несуществующие строки, извините), но это также блокирует чтение из вашей таблицы, пока вы не закончите.