5

我需要设置一个每分钟运行一次并在队列中发送电子邮件的自动化任务。我正在使用 ASP.NET 4.5 和 C#。目前,我使用从 global.asax 开始的调度程序类,并使用缓存和缓存回调。我读过这会导致几个问题。

我这样做的原因是因为这个应用程序在多个负载平衡的服务器上运行,这允许我在一个地方执行,即使一个或多个服务器脱机,代码也会运行。

我正在寻找一些方向来使这更好。我读过关于 Quartz.NET 但从未使用过它。Quartz.NET 是否从应用程序调用方法?还是从 Windows 服务?或来自网络服务?

我还阅读了有关使用 Windows 服务的信息,但据我所知,这些服务直接安装到服务器上。问题是,无论有多少服务器在线并且不想复制它,我都需要执行任务。例如,如果我在服务器 1 和服务器 2 上设置了计划任务,它们将一起运行,因此会复制请求。但是,如果服务器 1 离线,我需要服务器 2 来运行任务。

关于如何在此处继续前进的任何建议,或者 global.asax 方法是多服务器环境的最佳方法吗?顺便说一句,Web 服务器正在运行带有 IIS 8 的 Win Server 2012。

编辑

在请求更多信息时,队列存储在数据库中。我还应该提一下,数据库服务器与 Web 服务器是分开的。有两台数据库服务器,但一次只运行一台。他们都从中读取一个中央存储,因此只有一个数据库实例。当一个数据库服务器出现故障时,另一个会联机。

话虽如此,将 Windows 服务部署到两个数据库服务器是否更有意义?这将确保一次只运行一个。

另外,你对从应用程序运行 Quartz.NET 有什么想法?正如 millimoose 所提到的,我不一定需要它在 Web 前端运行,但是,这样做可以让我不必将 Windows 服务部署到多台机器上,而且我认为这两种方式都不会产生性能差异。想法?

到目前为止,感谢大家的投入。如果需要任何其他信息,请告诉我。

4

2 回答 2

6

我必须解决您现在面临的确切问题。

首先,您必须意识到您绝对不能在 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并且不访问任何全局数据结构,就可以了。

于 2013-02-06T20:17:22.333 回答
1

您必须更具体地了解您的架构。电子邮件队列在哪里;在内存或数据库中?如果它们存在于数据库中,您可以有一个名为“处理”的标志列,当一个任务从队列中抓取一封电子邮件时,它只抓取当前未处理的电子邮件,并将处理标志设置为 true 对于它抓取的电子邮件。然后,您将并发问题留给数据库。

于 2013-02-06T18:44:15.423 回答