正如Ozz已经提到的那样,常见的方法是消息队列。从设计的角度来看,消息队列实质上是FIFO队列,这是一种非常基本的数据类型:
使消息队列与众不同的原因是,虽然您的应用程序负责排队,但另一个进程将负责排队。在对语言进行排队时,您的应用程序是消息的发送者,而出队列过程是接收者。明显的优点是,整个过程是异步的,只要有消息要处理,接收者就独立于发送者工作。明显的缺点是您需要一个额外的组件,即发送者,才能使整个工作正常进行。
由于您的体系结构现在依赖于交换消息的两个组件,因此您可以为其使用幻想术语“ 进程间通信 ”。
引入队列如何影响您的应用程序设计?
您的应用程序中的某些操作会生成电子邮件。引入消息队列将意味着这些操作现在应改为将消息推送到队列(仅此而已)。这些消息应携带您的收件人处理邮件时构造电子邮件所必需的绝对最少信息量。
消息的格式和内容
消息的格式和内容完全取决于您,但是请记住,消息越小越好。您的队列应尽可能快地进行写入和处理,将大量数据丢给队列可能会造成瓶颈。
此外,一些基于云的排队服务对消息大小有限制,并且可能会拆分较大的消息。您不会注意到,当您要求拆分消息时,它们将被当作一个消息使用,但是您需要为多条消息付费(假设您使用的是收费服务)。
接收器设计
因为我们在谈论Web应用程序,所以对于您的接收者来说,一种通用的方法是使用简单的cron脚本。它每x
分钟(或几秒钟)运行一次,它将:
n
从队列中弹出消息数量,
- 处理消息(即发送电子邮件)。
请注意,我说的是pop而不是get或fetch,这是因为您的接收者不仅从队列中获取项目,还清除了它们(即,将它们从队列中删除或将它们标记为已处理)。究竟如何发生取决于消息队列的实现和应用程序的特定需求。
当然,我要描述的本质上是批处理操作,这是处理队列的最简单方法。根据您的需求,您可能希望以更复杂的方式处理消息(这也将需要更复杂的队列)。
交通
您的接收器可以考虑流量,并根据其运行时的流量来调整其处理的消息数。一种简单的方法是根据过去的交通数据来预测您的繁忙时间,并假设您使用的Cron脚本每x
分钟运行一次,则可以执行以下操作:
if(
now() > 2pm && now() < 7pm
) {
process(10);
} else {
process(100);
}
function process(count) {
for(i=0; i<=count; i++) {
message = dequeue();
mail(message)
}
}
这是一种非常幼稚和肮脏的方法,但是可以。如果不是这样,那么另一种方法是在每次迭代中找出服务器的当前流量,并相应地调整流程项的数量。如果不是绝对必要的话,请不要进行微优化,这样会浪费时间。
队列存储
如果您的应用程序已经使用数据库,那么在其上的单个表将是最简单的解决方案:
CREATE TABLE message_queue (
id int(11) NOT NULL AUTO_INCREMENT,
timestamp timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
processed enum('0','1') NOT NULL DEFAULT '0',
message varchar(255) NOT NULL,
PRIMARY KEY (id),
KEY timestamp (timestamp),
KEY processed (processed)
)
确实没有比这更复杂的了。当然,您可以根据需要使其变得复杂,例如,可以添加一个优先级字段(这意味着它不再是FIFO队列,但是如果您确实需要它,谁在乎?)。您也可以通过跳过已处理的字段来使其更简单(但是在处理完行之后,您必须删除行)。
数据库表对于每天2000条消息来说是理想的选择,但是对于每天数百万条消息来说,它可能无法很好地扩展。有上百万个因素需要考虑,基础架构中的所有内容都在应用程序的整体可伸缩性中发挥作用。
无论如何,假设您已经将基于数据库的队列识别为瓶颈,那么下一步将是查看基于云的服务。Amazon SQS是我使用的一项服务,它做了承诺。我敢肯定那里有很多类似的服务。
基于内存的队列也是要考虑的问题,尤其是对于短暂的队列。memcached非常适合作为消息队列存储。
无论您决定在其上建立队列的任何存储,都应使其聪明并抽象化。发送者和接收者都不应绑定到特定的存储,否则以后再切换到其他存储将是完整的PITA。
现实生活中的方法
我已经为电子邮件建立了一个与您正在做的非常相似的消息队列。它在一个PHP项目上,我围绕Zend Queue构建它,它是Zend Framework的一个组件,为不同的存储提供了多个适配器。我的存储位置:
- 用于单元测试的PHP数组,
- Amazon SQS投入生产,
- 在开发和测试环境上使用MySQL。
我的消息非常简单,我的应用程序创建了包含必要信息([user_id, reason]
)的小数组。消息存储是该数组的序列化版本(首先是PHP的内部序列化格式,然后是JSON,我不记得为什么切换了)。这reason
是一个常数,当然我在某个地方有一张大桌子,可以映射reason
到更完整的说明(我确实设法用隐秘的方式(reason
而不是一次完整的消息)向客户发送了大约500封电子邮件)。
进一步阅读
标准:
工具:
有趣的读物: