【问题标题】:Avoiding race conditions, but still being able to roll back避免竞争条件,但仍然能够回滚
【发布时间】:2023-03-24 09:20:01
【问题描述】:

我有一个 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 秒。

您对如何解决这个问题有任何想法吗?一种选择可能是检测表是否被锁定,然后跳过这整个步骤。这可能吗?

【问题讨论】:

  • 您是否因为无法在您的网络空间上使用 cronjobs 而在页面加载中检查这一点?

标签: php mysql email atomic race-condition


【解决方案1】:

如果您希望以任何方式扩展,请为此使用排队系统(redis、beantalkd、RabbitMQ 等)。

从长远来看,根据页面加载发送电子邮件是一个可怕的想法,因为您不是异步发送电子邮件,而是将随机用户的页面加载减慢很多。

这是一个例子:

获取一个 redis 队列,并发布一个 json 字符串,其中包括要从它们发送的电子邮件 ID:

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

创建一个 cronjob 来订阅此队列,并连接到数据库并发送带有这些 ID 的电子邮件。

如果出现错误,您只需将工作更改为"job":"errored"。在您的电子邮件任务的下一次计划运行中,您在那里处理它。

那里有很多队列库,在页面加载时执行异步任务是错误的方法。

【讨论】:

    【解决方案2】:

    回滚是个问题

    pending,successfull,failed 可能是 status 字段的可能值。

    您必须检查电子邮件发送失败,以便跟踪您实际发送的电子邮件(例如,发送的 100 封邮件中有 10 封已发送),尝试使用 Maildir 并检查新邮件中的关键字,例如 failure+@ 987654327@ 电子邮件然后相应地更新数据库。

    处理要发送的大量电子邮件。

    • 利用队列,可能是最安全的,在发送一组电子邮件后具有阈值和“冷却”。

    • 创建一个锁定文件以避免竞争,如果存在则进入睡眠,否则开始发送

    【讨论】:

      【解决方案3】:

      您可以在发送之前将其标记为待处理,在发送之后标记为发送。但是您仍然需要考虑在将其设置为 pending 和将其设置为 send 之间发生的情况。

      如果您不想并行发送多于 'x' 封电子邮件,那么您可以考虑在发送邮件之前重新计算标记为 pending 的条目。

      类似的东西:

      $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']");
          }
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2015-01-30
        • 2010-09-25
        • 2010-09-25
        • 2019-06-12
        • 1970-01-01
        • 2020-01-16
        • 2014-04-02
        相关资源
        最近更新 更多