合规国际互联网加速 OSASE为企业客户提供高速稳定SD-WAN国际加速解决方案。 广告
## 7.1 ABP后台服务 - 后台作业和后台工人 ### 7.1.1 简介 ABP提供了后台作业和后台工人,来执行应用程序中的后台线程的某些任务。 ### 7.1.2 后台作业 由于各种各样的原因,你需要后台作业以队列和持久化的方式来排队执行某些任务。 例如: * 用户等待执行一个长时任务。例如:某个用户按下了报表按钮生成一个需要长时间等待的报表。你添加这个工作到队列中,当报表生成完毕后,发送报表结果到该用户的邮箱。 * 重试创建并持久化任务操作,以保证任务的成功执行。例如:在后台作业中发送一封邮件,有些问题可能会导致发送失败(网络连接异常,或者主机宕机);由于有后台作业以及持久化机制,在问题排除后,可以重试执行失败的任务,以保证任务的成功执行。 > **关于作业持久化** > 了解更多信息可以查看源码:Background Job Store #### 1. 创建后台作业 通过继承 **BackgroundJob\<TArgs\>** 类或者直接实现 **IBackgroundJob\<TArgs\>** 接口,我们可以创建一个后台作业。 下面是一个极其简单的后台作业示例: ```csharp public class TestJob : BackgroundJob<int>, ITransientDependency { public override void Execute(int number) { Logger.Debug(number.ToString()); } } ``` 后台作业中定义了一个方法:**Execute** 并有一个输入参数。正如示例中所示,参数被定义为泛型参数。 后台作业必须使用[依赖注入](225827)进行注册,而实现 **ITransientDependency** 是最简单的方式。 下面我们将定义一个在后台队列中发送邮件的作业: ```csharp public class SimpleSendEmailJob : BackgroundJob<SimpleSendEmailJobArgs>, ITransientDependency { private readonly IRepository<User, long> _userRepository; private readonly IEmailSender _emailSender; public SimpleSendEmailJob(IRepository<User, long> userRepository, IEmailSender emailSender) { _userRepository = userRepository; _emailSender = emailSender; } public override void Execute(SimpleSendEmailJobArgs args) { var senderUser = _userRepository.Get(args.SenderUserId); var targetUser = _userRepository.Get(args.TargetUserId); _emailSender.Send(senderUser.EmailAddress, targetUser.EmailAddress, args.Subject, args.Body); } } ``` 我们注入了用户仓储(取得用户邮箱信息)和邮件发送服务,简单实现邮件的发送。在这里 **SimpleSendEmailJobArgs** 是作业参数,定义如下: ```csharp [Serializable] public class SimpleSendEmailJobArgs { public long SenderUserId { get; set; } public long TargetUserId { get; set; } public string Subject { get; set; } public string Body { get; set; } } ``` 作业参数应该是可序列化的,因为该参数需要被序列化并存储到数据库中。 在ABP中作业管理默认使用的是 **JSON** 序列化的方式(所以不需要添加[Serializable]特性)。当然最好是使用 **[Serializable]** 特性,因为我们可以自由的切换到其他作业管理中,以后可能会使用Binary序列化等等。 **参数应该做到简洁**,不应该包含实体或者其他非序列化的对象。正如 **SimpleSendEmailJob** 所展示的,我们应该仅存储实体的Id并从仓储内得到该实体的作业。 #### 2. 添加作业到队列中 在定义后台作业后,我们可以注入并使用 **IBackgroundJobManager** 接口来添加作业到队列中。 上面我们已经定义了TestJob类,让我们来看下如何使它排队: ```csharp public class MyService { private readonly IBackgroundJobManager _backgroundJobManager; public MyService(IBackgroundJobManager backgroundJobManager) { _backgroundJobManager = backgroundJobManager; } public void Test() { _backgroundJobManager.Enqueue<TestJob, int>(42); } } ``` 我们以42作为参数来排队。IBackgroundJobManager将会被实例化,并使用42这个参数来执行TestJob这个作业。 让我们为SimpleSendEmailJob添加一个新作业: ```csharp [AbpAuthorize] public class MyEmailAppService : ApplicationService, IMyEmailAppService { private readonly IBackgroundJobManager _backgroundJobManager; public MyEmailAppService(IBackgroundJobManager backgroundJobManager) { _backgroundJobManager = backgroundJobManager; } public async Task SendEmail(SendEmailInput input) { await _backgroundJobManager.EnqueueAsync<SimpleSendEmailJob, SimpleSendEmailJobArgs>( new SimpleSendEmailJobArgs { Subject = input.Subject, Body = input.Body, SenderUserId = AbpSession.GetUserId(), TargetUserId = input.TargetUserId }); } } ``` Enqueue (or EnqueueAsync) 方法还有其它参数如:**priority** 和 **delay**。 #### 3. 定义后台工作管理者 **BackgroundJobManager** 默认实现了IBackgroundJobManager接口。它能够被其它的后台作业提供者给替换掉(如:集成Hangfire)。在 **BackgroundJobManager** 中默认定义了一些信息: * 在单线程中,它是一种简单的 **FIFO(先进先出)** 的作业队列。使用 **IBackgroundJobStore** 来持久化作业。 * 对作业重试执行,直到作业执行成功(不抛出任何异常)或者操作超时。默认作业超时设置是2天。 * 当作业执行成功后,作业将会从数据库中删除。如果执行超时,那么该作业会被设置为 **abandoned** 并留在数据库中。 * 作业重试执行的时间间隔会慢慢增长。第一次重试是等待1分钟,第二次是等待2分钟,第三次是等待4分钟等等。 * 以固定的时间间隔轮询存储中的作业。查询作业是通过优先级和重试次数来排序的。 **后台作业存储** BackgroundJobManager 默认是需要数据存储来保存和检索作业的。如果你没有实现 **IBackgroundJobStore**,那么它会使用 **InMemoryBackgroundJobStore** 来存储作业,当然作业不会被持久化到数据库中。你可以简单的实现它来存储作业到数据库中或者你可以在[module-zero](/Markdown/AbpZero/1.1ABPZero-概述.md)直接使用,因为它已被实现。 #### 4. 配置 你可以在你的模块方法:**PreInitialize** 中,使用 **Configuration.BackgroundJobs** 配置后台作业系统。 **禁用后台作业** 你可能想在你的应用中禁用后台作业: ```csharp public class MyProjectWebModule : AbpModule { public override void PreInitialize() { Configuration.BackgroundJobs.IsJobExecutionEnabled = false; } //... } ``` 这种需求及其少见。但是想象一下,同时开启同一个应用程序的多个实例且使用的是同一个数据库。在这种情况下,每个应用程序的作业将查询相同的数据库并执行它们。这会导致相同的任务的多次执行,并还会导致其它的一些问题。为了阻止这个,你有两个选择: * 你可以仅为你应用程序的一个实例开启后台作业 * 你可以禁用Web应用的所有实例的后台作业,并且创建一个独立的应用程序(如:Windows服务)来执行你的后台作业。 #### 5. 集成Hangfire 后台作业管理者可以被其他后台作业管理者替换。详情请参考[集成Hangfire](225867) ### 7.1.3 后台工人 后台工人是不同于后台作业的。在应用程序中,它们是运行在后台单个线程中。一般来说,它们周期性的执行一些任务,例如: * 后台工人能够周期性地执行旧日志的删除 * 后台工人可以周期性地确定非活跃性用户并且发送邮件给这些用户,使这些用户返回到你的网站中。 #### 1. 创建后台工人 创建后台工人,我们应该实现 **IBackgroundWorker** 接口。根据你的需要,你可以继承 **BackgroundWorkerBase** 或者 **PeriodicBackgroundWorkerBase**。 假设有一个非活跃性的用户,该用户最近30天都没有访问我们的应用程序。 ```csharp public class MakeInactiveUsersPassiveWorker : PeriodicBackgroundWorkerBase, ISingletonDependency { private readonly IRepository<User, long> _userRepository; public MakeInactiveUsersPassiveWorker(AbpTimer timer, IRepository<User, long> userRepository) : base(timer) { _userRepository = userRepository; Timer.Period = 5000; //5 seconds (good for tests, but normally will be more) } [UnitOfWork] protected override void DoWork() { using (CurrentUnitOfWork.DisableFilter(AbpDataFilters.MayHaveTenant)) { var oneMonthAgo = Clock.Now.Subtract(TimeSpan.FromDays(30)); var inactiveUsers = _userRepository.GetAllList(u => u.IsActive && ((u.LastLoginTime < oneMonthAgo && u.LastLoginTime != null) || (u.CreationTime < oneMonthAgo && u.LastLoginTime == null)) ); foreach (var inactiveUser in inactiveUsers) { inactiveUser.IsActive = false; Logger.Info(inactiveUser + " made passive since he/she did not login in last 30 days."); } CurrentUnitOfWork.SaveChanges(); } } } ``` 这是一个真实性的代码,你可以在[module-zero](/Markdown/AbpZero/1.1ABPZero-概述.md)找到。 * 如果派生自 **PeriodicBackgroundWorkerBase**,你需要实现 **DoWork** 方法,在该方法中实现你周期性执行的逻辑。 * 如果派生自 **BackgroundWorkerBase** 或者直接实现 **IBackgroundWorker** 接口,你将要重写/实现 **Start,Stop, WaitStop** 方法。 Start 和 Stop 是非阻塞方法, WaitStop应该等待后台工人完成当前的重要任务。 #### 2. 注册后台工人 在创建后台工人后,我们应该添加它到 **IBackgroundWorkerManager**。通常是在我们的模块的 **PostInitialize** 方法中配置: ```csharp public class MyProjectWebModule : AbpModule { //... public override void PostInitialize() { var workManager = IocManager.Resolve<IBackgroundWorkerManager>(); workManager.Add(IocManager.Resolve<MakeInactiveUsersPassiveWorker>()); } } ``` 一般情况我们都是在PostInitialize做的,这没有严格的要求。你能够在任何地方注入IBackgroundWorkerManager并添加后台工人到运行时中。当你的应用程式停止后,IBackgroundWorkerManager将停止和释放所有被注册过的后台工人。 #### 3. 后台工人生命周期 后台工人通常是作为单例来执行的。这里也没有严格的限制,如果你需要同一个后台工人在多个实例中,你可以使它做为瞬时对象并添加多个实例到IBackgroundWorkerManager,在这种情况下,后台工人将被参数化(你有一个LogCleaner类,但是有2个后台LogCleaner工人实例,它们会看到和清除不同的日志文件夹)。 ### 7.1.4 使你的应用程序一直运行 后台作业和后台工人仅在你的应用程序运行的时候才工作。Asp.Net应用默认是关闭的, 如果web应用长时间没有被请求执行。所以,如果你的后台作业是宿主在Web应用中执行的,你应该确保你的web应用是被配置为一直执行的。否则,后台作业仅在你的web应用在使用的时候才会执行。 有一些技术可以实现这个,最简单的方法是用一个外部程序向你的web应用周期性的发送请求。因此,你也可以检测你的web应用是否启动并运行。[Hangfire文档](http://docs.hangfire.io/en/latest/deployment-to-production/making-aspnet-app-always-running.html)中描述了另外的实现方法。