在 asp.net-mvc 中,我如何在不影响用户体验的情况下运行一个耗时操作?

21

我有一个asp.net-mvc网站,正在使用nhibernate作为我的ORM。

我有一个当前的控制器操作,执行基本的CRUD更新(从数据库查询项目,然后更新一堆值并提交回db表)。 然后它返回一个简单的json响应给客户端,表示成功或错误。

 public ActionResult UpdateEntity(MyEntity newEntity)
 {
      var existingEntity = GetFromRepository(newEntity.Id);
      UpdateExistingEntity(newEntity, existingEntity);
      return Json(SuccessMessage);
 }
在某些情况下(假设提交成功并且更改了我的对象的某些字段),我现在想触发一些额外的操作(如给一些人发送电子邮件和运行生成报告的代码),但是我不想影响正在进行更新的用户体验。因此,我的担忧是如果我这样做:
 public ActionResult UpdateEntity(MyEntity newEntity)
 {
      var existingEntity = GetFromRepository(newEntity.Id);
      bool keyFieldsHaveChanged = UpdateExistingEntity(newEntity, existingEntity);
      if (keyFieldsHaveChanged)
     {
          GenerateEmails();
          GenerateReports();
     }
    return Json(SuccessMessage);
 }

有人想更新内容,但担心执行一个耗时操作会导致用户体验过慢。有没有什么方法(asyngc?)可以在控制器动作中触发一个昂贵的操作,但不会因此使控制器动作变慢?


5
您可以考虑将那些工作(电子邮件和报告)外包给一个独立的服务。 - IKEA Riot
@IKEA Riot - 但是我怎么能调用它呢...生成电子邮件和报告的代码在网站控制器中... - leora
3
在我所工作的一个应用程序中,我将一个标志写回到数据库。在这种情况下,一个单独的程序(系统服务)位于后台,轮询数据库以寻找工作。 - IKEA Riot
9个回答

51
我以前做过这件事。
最可靠的方法是使用异步控制器,或者更好的方法是使用独立服务,例如 WCF 服务。
但是根据我的经验,我只需要完成“简单”的一行任务,例如审计或报告,就像您所说的那样。
在这个例子中,简单的方法是启动一个 "Task":
public ActionResult Do()
{
    SomethingImportantThatNeedsToBeSynchronous();

    Task.Factory.StartNew(() => 
    {
       AuditThatTheUserShouldntCareOrWaitFor();
       SomeOtherOperationTheUserDoesntCareAbout();
    });

    return View();

}

这只是一个简单的例子。您可以启动任意数量的任务,同步它们,在它们完成时得到通知等。

我目前正在使用上述方法进行亚马逊 S3 上传。


3
@RPM1984 - 所以,为了澄清,你在返回视图之前并没有等待任务完成..是吗? - leora
@RPM1984 - 非常感谢。我没意识到你能做到这一点。我以为这可能会打破“管道”,在某种意义上超出范围。 - leora
5
标记这个作为答案。RPM1884应该获得赏金。 - Wouter Janssens
2
@cpoDesign - 在我提供的示例中,你不能这样做。但是通过编写更多的代码,你可以实现,就像我在上面的评论中所说的那样。 - RPM1984
在4.5版本中不再起作用。您将收到“此时无法启动异步操作”错误。 - Kugel
显示剩余6条评论

16
如果意图是立即返回JSON,并在后台异步运行密集工作而不影响用户体验,那么您需要启动一个新的后台线程。AsyncController在这里无法帮助您。有很多关于通过这样做使线程请求池饥饿的争论(这是真实的),但反驳的观点是因为服务器正在忙于工作,所以应该使线程请求池饥饿。当然,理想情况下,您应该通过排队/分布式系统等将工作移至另一台完全不同的服务器,但这是一个复杂的解决方案。除非您需要处理数百个请求,否则您不需要考虑此选项,因为它很少会引起问题。这真的取决于您的解决方案的可扩展性要求、后台进程需要花费多长时间以及调用频率如何。最简单的解决方案如下:
public ActionResult UpdateEntity(MyEntity newEntity)
{
    var existingEntity = GetFromRepository(newEntity.Id);
    bool keyFieldsHaveChanged = UpdateExistingEntity(newEntity, existingEntity);
    if (keyFieldsHaveChanged)
    {
        ThreadPool.QueueUserWorkItem(o =>
                                        {
                                            GenerateEmails();
                                            GenerateReports();
                                        });

    }
    return Json(SuccessMessage);
}

5
您应该使用异步操作和异步控制器,以避免锁定线程池并使网站的其他用户受到影响。当任务运行时间较长时,从ASP.NET线程池中获取的线程将被保留,并在操作完成之前不会返回到池中。如果有许多同时运行的长时间任务,则它们将保留许多线程,因此很有可能其他访问您网站的用户会等待而受到影响。异步操作不会使任何代码更快。我建议您仅在上述线程情况下使用异步控制器,但这还不够。我认为您应该使用一些链接或Ajax来触发服务器上的操作,并让用户继续浏览网站。一旦操作完成,在下一页刷新时,用户应该得到通知任务已经执行完毕。这是业务代码不应该写在控制器中的另一个证明。您应该有单独的服务来处理此类代码。

谢谢您的回复。您能否澄清如何既向用户返回值,又保持异步操作运行? - leora
最好的方法是像@Paolo Moretti建议的那样。控制器应该很小,只更新模型。然后应用程序中的某些其他服务,可能根本不是Web应用程序 - Windows服务或一些调度服务可以查看更新的模型并执行其任务。有太多的方法来做这种事情 - 它们取决于您的具体系统。但首先,请忘记控制器操作应执行此工作。只需将此过程视为触发长时间运行的任务和执行它之间的分离即可。由控制器触发。由其他服务执行。 - archil
你是在暗示这个“服务”代码必须放在我的网站之外,或者说只是另一个程序集或类吗?有没有任何解决方案可以让我从我的网站上运行代码(这样我就不必管理一个单独且独立的服务来运行本质上只是一个昂贵方法的代码开销)。 - leora
好的,就像我说的一样,一切都取决于你的系统。如果我不想让用户等待我的请求,我会处理单独服务的开销。最简单的方法是只需启动新的Thread()并将结果返回给用户。然后在该线程中执行您的任务。但再次强调,您应该非常注意此任务运行的细节 - 如果发生异常,某些子任务失败等。 Thread t = new Thread(ExecuteLongAction); t.Start(); return Content("Update Submitted"); //这将启动ExecuteLongAction方法,但在方法执行完成之前向用户返回响应 - archil

3

这是一个老问题,因此我认为它需要更新。

我建议使用HangFire (http://hangfire.io)。使用它,您可以在Web应用程序中轻松排队作业。HangFire将确保作业至少运行一次。

// Static methods are for demo purposes
BackgroundJob.Enqueue(
    () => Console.WriteLine("Simple!"));

您可以在漂亮的用户界面中查看所有排队作业的状态。

你是在说现在已经不正确了吗? - leora
不,它是正确的。我的答案只是解决Web应用程序内长时间运行任务更新(而且我认为更优雅)的选择 :-) - Martin Brabec
1
@LyubomirVelchev 它是开源的,基本功能免费使用。对于更高级的场景,您可能需要升级到高级版。 - hbulens

2
我认为你真正想要的是类似于Windows Azure的Worker Role。

http://www.microsoft.com/windowsazure/features/compute/

我不确定如何在纯MVC中最好地实现它,没有Azure队列。并且,根据您的托管位置(互联网托管商、具有root权限的自己的服务器等),会有一些复杂性。

1

1

如果我理解你的问题正确,这并不主要是关于异步操作。这是关于长时间运行的操作。您应该将长时间运行的操作卸载到后台作业中。ASP.NET 应用程序不适合执行后台作业。我可以看到您可以选择以下几个选项:

  1. Windows 服务 - 这可以轮询您的数据库以获取某种状态,并从那个状态开始触发操作
  2. WCF 服务 - 您的 ASP.NET 应用程序可以发送异步请求到 WCF 服务,而无需等待响应
  3. 还可能有其他选项,如 BizTalk,但这取决于您的应用程序结构等。

从用户界面,您应该能够定时轮询以向用户提供状态(如果您认为用户需要立即了解该状态),或在操作完成时向用户发送电子邮件。

顺便说一下,重要的是使用异步操作执行您的 I/O,其他人已经提供了一些关于如何实现这一点的好链接。


1

如果您考虑到用户体验(用户不应该等待这些任务的执行)和可靠性(即使应用程序重新初始化,任务也必须被排队和执行),您可以选择使用MSMQ。

MSMQ为异步操作提供了一个强大的解决方案。它支持同步操作,提供日志和托管API,并且其主要关注点正是这种情况。

请查看以下文章:

MSMQ简介

.Net中的MSMQ编程


0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接