通过cron作业发送电子邮件的实现


Implementation of sending emails through a cron job

考虑数据库中的以下'pending_send_confirm'表,用于存储我需要发送电子邮件的电子邮件地址(最终用户将通过该电子邮件确认他们的订阅):

id| address |occupied|
0 |em1@s.com|0       |
1 |em2@s.com|0       |
2 |em3@s.com|0       |

当用户订阅(例如我的时事通讯)时,我想显示一条消息,说一切都很好。我不希望他在显示消息之前等待服务器将确认电子邮件发送到他的地址。

这就是为什么我设置了一个每分钟运行一次的cron作业,以便管理必须发送的电子邮件。根据我想出的表格(如上所示),这是一些向每个地址发送电子邮件的伪代码,1乘1:

//child_script.php
//fetch the entries from the database
while ($entry = $result->fetch_assoc()) {
    if ($entry['occupied']) {
        /*
         * Another instance of this script has 'occupied' this address, which
         * means that it is currently trying to send a confirmation email to
         * this address. So you know that another instance is working with this
         * email address, so skip it.
        */
        continue;
    }
    /*
     * Entry is not occupied, occupy it now in order to prevent future instances
     * of this script to attempt to send additional confirmation email to this address
     * while this instance of the script tries to send the confirmation email to this address.
     * occupied=1 means that an attempt to send the confirmation email is under the way
    */
    occupyEntry($entry['id']); //sets 'occupied' to 1
    if (sendConfirmationEmail($entry['address'])) {
        /*
         * Email was sent successfully, move the email address from the 'pending_send_confirm' to the 
         * 'pending_confirmation_from_user' table.
        */
        moveToConfirmPendingFromUserTable($entry['id']);
    } else {
        /*
         * Failed to send the email, unoccupy the entry so another instance of the script
         * can try again sometime in the future
        */
        unoccupyEntry($entry['id']); //sets 'occupied' to 0
    }

}

无注释可读性代码:

//child_script.php
while ($entry = $result->fetch_assoc()) {
    if ($entry['occupied']) {
        continue;
    }
    occupyEntry($entry['id']);
    if (sendConfirmationEmail($entry['address'])) {
        moveToConfirmPendingTable($entry['id']);
    } else {
        unoccupyEntry($entry['id']);
    }
}

这是防止重复发送电子邮件的可靠解决方案吗?我担心脚本的两个实例可能会"同时"发现特定id的$entry['occupied']为0,并尝试同时向该地址发送电子邮件。

另一个解决方案是使用flock(我如何确保只有一个PHP脚本实例通过Apache运行?),以确保我的脚本只有一个实例在运行。

不过,我可以看到羊群实现中存在许多问题。例如,如果我的脚本在调用fclose($fp)之前崩溃,会发生什么?我的脚本的下一个实例是否能够继续,或者它是否将其视为正在运行的脚本的另一个实例(即flock函数将向脚本的新实例返回什么)?另一个问题是,我的脚本一个接一个地发送电子邮件。这意味着,如果我有100封电子邮件要发送,并且我在3.5分钟内发送,那么下一个实例将在第一个实例启动后4分钟启动。这意味着,如果订阅者在第一个实例启动的那一刻选择订阅,那么他们将不得不等待4分钟以上才能收到确认电子邮件。如果我允许脚本并行工作,那么电子邮件的发送速度会更快。因此,我更希望能够拥有同一脚本的多个实例,同时发送电子邮件。

顺便说一句,如果我的脚本使用"占领"方法运行良好,我能让另一个脚本管理将要启动的同时实例的数量吗?例如,我能做以下事情吗?:

//master_script.php
/*
 * Launch many instances of child_script.php.
 * If there are 901 to 1000 emails, then start 10 instances etc
*/
launch_n_instances( ceil(number_of_non_occupied_entries()/100.0) );

处理这个问题的正确方法是什么?

在另一个表中创建一个名为"cron_running"或类似的字段,并在cron脚本的顶部将其设置为true。在脚本结束时,将字段设置回false。这将允许您检查cron脚本是否仍在运行,如果它碰巧重叠而没有继续。

当阅读有关您的问题时,会出现许多问题:

你说:

我不希望他在显示消息之前等待服务器将确认电子邮件发送到他的地址。

首先,发送确认邮件所增加的延迟应该非常短。

其次,对我来说,订阅时事通讯很像是一件"电子邮件的事情"。如果你没有得到一些确认,你怎么能说"订阅进展顺利"呢?

事实上,你提到的另一个问题可以归结为如何为数据库中的多个进程记录某些状态——在本例中是"发送电子邮件"。如果您的数据库具有适当的隔离级别,即您可以只读取未更改的内容,则数据库会处理此问题。

您的"占用"字段可以称为email_status,其值为

0 - new
1 - processing
2 - created
3 - confirmed 

最后一点是,这种记账只是一种暂时的状态。您不想在数据库中保留所有多余的"已确认"条目,只是为了获得从"新"到"已确认的"过程中可能出现的错误的信息。

因此,您甚至可以使用一个不同的表,由id和status组成,其中处理的最后一步是删除它们。这样,您的附加表将只包含未完全处理的ID。