php - cronjob 是使用 SMTP 通过 PHPMailer 发送大量通知电子邮件的正确方法吗?

标签 php mysql email cron phpmailer

我有一个网络应用程序 (PHP/MYSQL),每次有人通过我的应用程序中的表单在我的数据库中提交条目时,它都会向与该条目关联且选择接收通知的每个人发送一封通知电子邮件。

例如,有一个可以容纳 15 人的橄榄球采摘池。当第 16 个人提交他们的参赛作品时,0-15 人之间的任何地方都会收到有新参赛作品的通知。可能是 1 个,可能是全部 15 个,可能是其中的一半左右。

但我的站点可能有 100 个足球池,一些有 5 人,一些有 100 人。

所以我设置了一个单独的表来记录每个条目的基本信息,并将“已发送”列设置为“0”。

然后我让下面的 cronjob 每分钟运行一次,它会查找所有具有“0”的条目(这意味着与该条目关联的人尚未收到通知)并循环并向每个条目发送一封唯一的电子邮件关联的人。我发送独特的电子邮件,因为每封电子邮件都有一个退订链接和特定于该人的信息。发送电子邮件后,它将所有这些条目的“已发送”列更新为“1”,因此它将被忽略。

我通过 Amazon SES 使用 PHPMailer SMTP 发送电子邮件。

<?php
require_once("includes/session.php");
require_once("includes/connection.php");
require_once("includes/functions.php");
require 'phpmailer/PHPMailerAutoload.php';

//find all new entries with sent = 0, loop thru and send emails
$stmt = $pdo->prepare("SELECT * FROM cron_email_notify WHERE sent = 0");
$stmt->execute();
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
    //bunch of variables set here (which i'll leave out for brevity's sake) to be used in the emails (like firstname, etc)

        //Next, send a unique email to all people who have chosen to be notified
        $stmt = $pdo->prepare("SELECT * FROM entries WHERE poolid = ? AND notify = 'yes'");
        $stmt->execute([$poolid]);
        foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
            //get emails into array
            $notifyemailsarray[$row['email']]=$row;
        }

        /// Send notification email to all people who wish to be notified, if any
        $mail = new PHPMailer;
        //$mail->SMTPDebug = 3;                               // Enable verbose debug output
        $mail->isSMTP();                                      // Set mailer to use SMTP
        $mail->SMTPKeepAlive = true;  //Helps with speed for multiple emails
        $mail->Host = 'tls://email-smtp.us-east-1.amazonaws.com';  // Specify main and backup SMTP servers
        $mail->SMTPAuth = true;                               // Enable SMTP authentication
        // and so on with other phpmailer info which I cut out for brevity's sake

        if (!empty($notifyemailsarray)) {
            foreach($notifyemailsarray as $email => $details)   {
                // Assemble the fullname here
               $fullname = $details['firstname'] . ' ' . $details['lastname'];
               $mail->addAddress($details['email'], $fullname);
               $mail->Body    =  "(leaving out for brevity's sake)";
               $mail->AltBody = "(leaving out for brevity's sake)"; 
                if(!$mail->send()) {
                    echo 'Message could not be sent.';
                    echo 'Mailer Error: ' . $mail->ErrorInfo;
                } else {
                    //email sent
                }
                // Clear all addresses for next loop
                $mail->clearAddresses();
            }
        } else {
            //no people  are set to be notified 
        }
        $mail->SmtpClose();  
    //update cron_email_notify table's sent field to 1, so we know not to send again
    $stmt = $pdo->prepare("UPDATE cron_email_notify SET sent = 1 WHERE poolid = ? and sent = 0");
    $stmt->execute([$poolid]);
}
?>

在测试中一切正常,但我只测试了它发送大约 10 封电子邮件的地方(即小土 bean )

我的应用程序将收到大量提交,并且随着时间的推移应该会不断增长,所以我只是想看看这是否是一个潜在的问题(我对在后台运行 cronjobs 的经验很少,所以不是确定要影响网站性能的操作/流量有多疯狂)。

我很担心,因为发送 10-15 封电子邮件时,上面的 cronjob 脚本似乎需要大约 5 秒才能完成。

现在,假设有大量条目涌入后,我的实时网站启动了一个 cronjob,它必须发送 200 封电子邮件。如果 cronjob 需要一分钟的时间才能运行,下一个 cronjob 将开始(因为它们每分钟运行一次),这难道不会让我的系统完全陷入困境吗?或者同一个 cronjob 可以在另一个 cronjob 运行时没有问题吗?

显然,许多网站都会发送大量通知电子邮件,所以这一定是一个普遍的需求,我确信有一个很好的解决方案,所以只是好奇任何人的想法/建议/等等。也许我这样做的方式很好,直到我获得如此多的流量,这将是一个很大的问题。或者也许我现在可以做更多的事情来帮助抵消可能陷入困境的系统?

最佳答案

我在一家公司工作,该公司为美国一半以上的公立学校发送通知。我们每天发送大约 1000 万封电子邮件。

您的代码是单线程的,因此它会连续发送电子邮件,一封接一封,这可能需要一段时间。你问如果它花费的时间比你的 cron 作业之间的间隔时间长会发生什么?也就是说,如果您的 cron 作业每分钟运行一次,但在某个时候它会在前一个脚本仍在运行时启动您的脚本。然后,您的脚本的两次调用都将处理同一批电子邮件,您的用户将开始收到重复的电子邮件。

因此,最好运行一个守护进程。这只是意味着不是 cron 每分钟启动 PHP 脚本,而是编写 PHP 脚本以运行 while (true) 循环并且永不退出。在循环结束时,使脚本 sleep() 最多 60 秒。然后它转到循环的顶部,检查数据库以查看是否有要运行的电子邮件批处理,然后处理该批处理。然后它重新进入休眠状态,持续的秒数等于 60 减去处理批处理所花费的时间(但不少于零秒)。

这样一来,如果出现激增,即使一批电子邮件需要 75 秒,它也永远不会同时运行两个应用程序。您的守护进程的 sleep 计算将使它至少 sleep 0 秒,然后它会立即开始下一批。希望激增是暂时的,应用程序可以在某个时候 catch 。否则,您的应用将越来越落后于计划。

您的应用应将发送的电子邮件数量以及批处理之间休眠的时间写入日志文件。您应该监控此文件,以便在它无法满足需求时知道。一旦您变得真正专业,您就会将这些数据放入 Grafana 仪表板或类似的东西中。

如果您的流量增长到单线程应用始终无法在 60 秒内发送一批电子邮件的程度,那么您将不得不并行运行多个电子邮件发件人应用。 PHP 在多线程编程方面表现不佳,因此您可能希望使用 Java 或 Go。

尝试让多个线程轮询数据库是个坏主意,因为您会遇到竞争条件和锁争用。我们在我公司所做的是让一个线程轮询数据库。当它找到一批要发送的电子邮件时,它会将它们推送到消息队列中(我们使用 ActiveMQ,但还有其他的)。消息队列使多个线程可以轻松地从队列中拉出项目。然后,多个并发电子邮件发送应用程序线程就可以轻松地并行处理成批电子邮件。

P.S.:您是否意识到,即使部分或全部电子邮件发送失败,您的代码也会将池标记为 sent=1

关于php - cronjob 是使用 SMTP 通过 PHPMailer 发送大量通知电子邮件的正确方法吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47584277/

相关文章:

php - 这是从服务层组织类的好方法吗?

php - 如何使用 PHP 正则表达式压缩空格字符?

MySQL正则表达式用于包含 '@'的字边界

mysql - c9.io - 如何在 node.js 平台中找到主机地址以建立 mysql 连接

mysql - 重新启动Docker Compose并更新数据库

linux - 更改 Plesk 12 管理员电子邮件

php - bind_param() 问题

php - 在 PHP 中的静态方法中访问全局变量

html - Win 10 UWP EmailMessage API 是否支持 HTML 正文?

php - 我可以通过仅替换 CR 来避免 CRLF 注入(inject)攻击吗?