Избегать условий гонки, но все еще в состоянии откатиться

У меня есть таблица MySQL с электронными письмами, которые должны быть отправлены.

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

Чтобы две одновременные загрузки страниц не отправляли одно и то же электронное письмо, я думаю сделать что-то вроде этого:

$pdo = new PDO(...);

// Start blocking other page loads
$pdo->beginTransaction();
$stmt = $pdo->query("SELECT id, recipient, subject, body
FROM emails WHERE sent = 0 LIMIT 1 FOR UPDATE");

$mail = $stmt->fetch();

if(false !== $mail)
$pdo->exec("UPDATE emails SET sent = 1 WHERE id = $mail['id']");

// End blocking other page loads
$pdo->commit();

if(false !== $mail) {
// Send e-mail
}

Но что, если выполнение будет прервано после фиксации, но до того, как электронное письмо будет успешно отправлено? Письмо будет отправлено на рынок, но на самом деле отправлено не будет. Конечно, я могу подождать с фиксацией до того, как письмо будет отправлено, но это приведет к гораздо более длительному периоду блокировки. Я отправляю электронные письма по SMTP, а отправка одного письма занимает около 10 секунд.

У вас есть идеи, как это решить? Одним из вариантов может быть обнаружение блокировки таблицы, а затем просто пропустить весь этот шаг. Это возможно?

3

Решение

Используйте для этого систему очередей (redis, beanstalkd, RabbitMQ и т. Д.), Если вы хотите, чтобы это каким-либо образом масштабировалось.

Отправка электронных писем на основе загрузки страниц — ужасная идея в долгосрочной перспективе, так как вы не отправляете электронные письма асинхронно, но замедляет загрузку страницы случайных пользователей на много.

Вот пример:

Возьмите очередь redis и опубликуйте строку json, включая идентификаторы электронной почты, которые будут отправлены с них:

{"id":1, "job":"pending", "data": {"user": "foobar"}}

Сделайте cronjob, чтобы подписаться на эту очередь, подключиться к базе данных и отправлять электронные письма с этими идентификаторами.

Если есть ошибка, вы просто меняете работу на "job":"errored", При следующем запланированном запуске задачи электронной почты вы справитесь с ней там.

Существует довольно много библиотек очередей, и выполнение асинхронных задач при загрузке страницы — неправильный способ сделать это.

2

Другие решения

Откат — это проблема

pending,successfull,failed могут быть возможные значения для status поле.

Вы должны проверить ошибки доставки электронной почты, чтобы отслеживать фактическую доставку электронной почты (например, 10 из 100 отправленных), попробуйте поработать с Maildir и проверить наличие новых писем по таким ключевым словам failure+xyz@xyz.xyz по электронной почте, затем обновите базу данных соответственно.

Работа с большим количеством писем для отправки.

  • использовать очереди, вероятно, самые безопасные с порогом и «перезарядкой» после того, как группа писем была отправлена.

  • создать файл блокировки, чтобы избежать гонки, если он существует, тогда иди спать, а затем начать отправлять

0

Вы могли бы отметить это как в ожидании прежде чем отправить его и как Отправить после того как вы отправили его. Но вам все еще нужно подумать о ситуации, когда что-то происходит между установкой в ожидании и установив его Отправить.

Если вы не хотите отправлять больше, чем «x» электронных писем параллельно, то вы можете подумать над подсчетом записей, помеченных как в ожидании прежде чем отправлять почту.

Что-то вроде того:

$pdo = new PDO(...);

// Start blocking other page loads
$pdo->beginTransaction();
$stmt = $pdo->query("SELECT id, recipient, subject, body
FROM emails WHERE status = 'queued' LIMIT 1 FOR UPDATE");

$mail = $stmt->fetch();

if(false !== $mail)
$pdo->exec("UPDATE emails SET status = 'pending' WHERE id = $mail['id']");

// End blocking other page loads
$pdo->commit();

if(false !== $mail) {
// Send e-mail
if( $successfull ) {
$pdo->exec("UPDATE emails SET status = 'sended' WHERE id = $mail['id']");
} else {
$pdo->exec("UPDATE emails SET status = 'failed' WHERE id = $mail['id']");
}
}
0