我必须解决您现在面临的确切问题。
首先,您必须意识到您绝对不能在 ASP.NET 中可靠地运行长时间运行的进程。如果从 global.asax 实例化调度程序类,则无法控制该类的生命周期。
换句话说,IIS 可能会决定随时回收承载您的类的工作进程。充其量,这意味着您的课程将被摧毁(您对此无能为力)。在最坏的情况下,你的班级会在工作中被杀。哎呀。
运行长期进程的适当方法是在机器上安装 Windows 服务。我会在每个网络盒子上安装该服务,而不是在数据库上。
Service 实例化 Quartz 调度程序。这样,您就知道只要机器启动,您的调度程序就可以保证继续运行。当需要运行作业时,Quartz 只需调用IJob
您指定的类的方法。
class EmailSender : Quartz.IJob
{
public void Execute(JobExecutionContext context)
{
// send your emails here
}
}
请记住,QuartzExecute
在单独的线程上调用该方法,因此您必须小心线程安全。
当然,您现在将在多台机器上运行相同的服务。虽然听起来您对此感到担忧,但实际上您可以将其转化为积极的事情!
我所做的是在我的数据库中添加一个“锁定”列。当发送作业执行时,它通过设置锁定列来获取队列中特定电子邮件的锁定。例如,当作业执行时,生成一个 guid,然后:
UPDATE EmailQueue SET Lock=someGuid WHERE Lock IS NULL LIMIT 1;
SELECT * FROM EmailQueue WHERE Lock=someGuid;
这样,您就让数据库服务器处理并发。该UPDATE
查询告诉数据库将队列中的一封电子邮件(当前未分配)分配给当前实例。然后,您SELECT
将锁定的电子邮件发送出去。发送后,从队列中删除电子邮件(或者您处理已发送的电子邮件),然后重复该过程,直到队列为空。
现在您可以在两个方向上进行缩放:
- 通过同时在多个线程上运行相同的作业。
- 由于这是在多台机器上运行的事实,您可以有效地在所有服务器上平衡您的发送工作。
由于锁定机制,您可以保证队列中的每封电子邮件只发送一次,即使多台机器上的多个线程都运行相同的代码。
回应评论:我最终得到的实现存在一些差异。
首先,我的 ASP 应用程序可以通知服务队列中有新电子邮件。这意味着我什至不必按计划运行,我可以简单地告诉服务何时开始工作。但是,这种通知机制很难在分布式环境中正确使用,因此只需每分钟左右检查一次队列就可以了。
您使用的时间间隔实际上取决于您的电子邮件传递的时间敏感性。如果需要尽快发送电子邮件,您可能需要每 30 秒或更短时间触发一次。如果不是那么紧急,您可以每 5 分钟检查一次。Quartz 限制了一次执行的作业数量(可配置),并且您可以配置如果错过触发器应该发生什么,因此您不必担心有数百个作业备份。
其次,我实际上一次锁定 5 封电子邮件,以减少数据库服务器上的查询负载。我处理大量事务,因此这有助于提高效率(服务和数据库之间的网络往返次数减少)。这里要注意的是,如果一个节点在发送一组电子邮件的过程中发生故障(无论出于何种原因,从异常到机器本身崩溃),会发生什么。您最终会在数据库中得到“锁定”的行,并且没有任何服务可以为它们服务。群体规模越大,这种风险就越大。此外,如果所有剩余的电子邮件都被锁定,则空闲节点显然无法处理任何事情。
至于线程安全,我的意思是一般意义上的。Quartz 维护了一个线程池,因此您不必担心实际管理线程本身。
你必须小心你工作中的代码访问什么。根据经验,局部变量应该没问题。但是,如果您访问函数范围之外的任何内容,线程安全是一个真正的问题。例如:
class EmailSender : IJob {
static int counter = 0;
public void Execute(JobExecutionContext context) {
counter++; // BAD!
}
}
此代码不是线程安全的,因为多个线程可能会尝试同时访问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
并且不访问任何全局数据结构,就可以了。