我曾经处理过你现在面临的确切问题。
首先,你必须意识到,在ASP.NET中无法可靠地运行长时间运行的进程。如果你从global.asax实例化调度器类,那么你就无法控制该类的生命周期。
换句话说,IIS可能会随时回收托管你类的工作进程。最好的情况是,你的类将被销毁(你无能为力)。最坏的情况是,在进行工作的过程中,你的类将被终止。糟糕透了。
运行长期进程的适当方法是在计算机上安装Windows服务。我会在每个Web服务器上安装服务,而不是在数据库上安装。
服务实例化Quartz调度程序。这样,你就知道只要机器开着,你的调度程序就保证会继续运行。当需要运行作业时,Quartz只需调用你指定的IJob
类的一个方法即可。
class EmailSender : Quartz.IJob
{
public void Execute(JobExecutionContext context)
{
}
}
请记住,Quartz在单独的线程上调用
Execute
方法,因此您必须小心保证线程安全。
当然,现在您将在多台机器上运行相同的服务。虽然听起来您很担心这个问题,但实际上您可以将其转化为积极的事情!
我所做的是在我的数据库中添加了一个"锁定"列。当发送作业执行时,它通过设置锁定列来锁定队列中特定的电子邮件。例如,当作业执行时,生成一个GUID,然后:
UPDATE EmailQueue SET Lock=someGuid WHERE Lock IS NULL LIMIT 1;
SELECT * FROM EmailQueue WHERE Lock=someGuid;
以这种方式,您让数据库服务器处理并发。
UPDATE
查询告诉 DB 将队列中的一个电子邮件(当前未分配)分配给当前实例。 然后
SELECT
锁定的电子邮件并发送它。 发送后,从队列中删除电子邮件(或者按照您处理已发送电子邮件的方式),并重复该过程,直到队列为空。
现在您可以朝两个方向进行扩展:
- 通过同时运行多个线程来运行相同的作业。
- 由于此代码正在多台计算机上运行,因此您有效地在所有服务器上平衡了发送工作负载。
由于锁定机制,即使多个线程在多台计算机上运行相同的代码,您也可以保证队列中的每个电子邮件仅发送一次。
回应评论: 我最终实现的方法有几个不同之处。
首先,我的ASP应用程序可以通知服务队列中有新电子邮件。这意味着我甚至不必按计划运行,我只需告诉服务何时开始工作即可。然而,在分布式环境中,这种通知机制非常难以正确实现,因此每分钟检查一次队列应该就可以了。
你选择的间隔时间取决于电子邮件传递的时间敏感性。如果需要尽快发送电子邮件,则可能需要每30秒或更短时间触发一次。如果不那么紧急,则可以每5分钟检查一次。Quartz限制同时执行的作业数量(可配置),您可以配置如果触发器被错过应该发生什么,因此您不必担心有数百个作业积压。
其次,我实际上一次锁定5封电子邮件,以减少对DB服务器的查询负载。由于处理的数据量很大,因此这有助于提高效率(服务和DB之间的网络往返次数更少)。需要注意的是,如果在发送一组电子邮件时某个节点(无论出于什么原因,从异常到机器崩溃)意外关闭,则会在DB中留下“锁定”行,并且没有任何服务可以为其提供服务。组的大小越大,风险就越大。此外,如果所有剩余电子邮件都被锁定,则空闲节点显然无法处理任何内容。
至于线程安全性,我指的是
一般意义。Quartz维护一个线程池,因此您不必担心实际管理线程本身。
您必须小心您的作业中的代码所访问的内容。作为经验法则,局部变量应该没问题。但是,如果您访问函数范围之外的任何内容,则线程安全性是一个真正的问题。例如:
class EmailSender : IJob {
static int counter = 0;
public void Execute(JobExecutionContext context) {
counter++;
}
}
这段代码不是线程安全的,因为多个线程可能同时尝试访问counter
。
Thread A Thread B
Execute()
Execute()
Get counter (0)
Get counter (0)
Increment (1)
Increment (1)
Store value
Store value
counter = 1
counter
应该是2,但是我们却遇到了一个极难调试的竞态条件。下一次运行这段代码时,可能会出现以下情况:
Thread A Thread B
Execute()
Execute()
Get counter (0)
Increment (1)
Store value
Get counter (1)
Increment (2)
Store value
counter = 2
...而你只是一头雾水,不明白这次为什么可以正常工作。
在你的特定情况下,只要在每次调用Execute
时创建一个新的数据库连接并且不访问任何全局数据结构,你就应该没问题。