本节内容:
ABP提供了后台作业和工作者,用来在后台线程里执行应用里的某些任务。
后台作业用一种队列且持久稳固的方式安排一些待执行后台任务,你可能有几个理由,需要用到后台作业,例如:
- 为执行长时间运行的任务而用户无需等待,例如:用户按了一下“报告”按钮开始一个长时间运行的报告任务,你把这个任务添加到队列里,它任务完成后,通过电子邮件发送报告结果给你。
- 为创建可重试且持久稳固的任务来保证一个代码将会被完成运行,例如:你可以在后台作业里发送一个电子邮件,为克服临时的失败且保证它最终将会被发送,当然当发送电子邮件时用户也无需等待。
查看后台作业存储小节获取有关作业持久化的更多信息。
我们可以通过继承BackgroundJob<TArgs>类或直接实现IBackgroundJob<TArgs>接口来创建一个后台作业。
下列为一个非常简单的后台作业:
public class TestJob : BackgroundJob<int>, ITransientDependency { public override void Execute(int number) { Logger.Debug(number.ToString()); } }
一个后台作业定义了一个Execute方法,接受一个输入参数,参数类型就是定义泛型类的参数,如上例所示。
一个后台作业必须注册到依赖注入,实现ITransientDependency是最简单的方式。
让我们定义一个更真实的作业:在后台队列里发送电子邮件:
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); } }
我们注入user仓储(可获取用户电子邮件)和邮件发送器(一个发送邮件的服务),并简单的发送邮件,SimpleSendEmailJobArgs是作业的参数,它的定义如下所示:
[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],因为将来我们可能会替换成另一个作业管理器,可能会使用.net内置的二进制序列化器。
保存你的参数简单(如DTO),不要包含实体或其它非序列化对象,如所示的SimpleSendEmailJob,我们可以只存储一个实体的Id,并通过它从作业内部的仓储里获取实体。
在定义完一个后台作业之后,我们可以注入并使用IBackgroundJobManager给队列添加一个作业,看一下使用上面已定义的TestJob的例子:
public class MyService { private readonly IBackgroundJobManager _backgroundJobManager; public MyService(IBackgroundJobManager backgroundJobManager) { _backgroundJobManager = backgroundJobManager; } public void Test() { _backgroundJobManager.Enqueue<TestJob, int>(42); } }
当加入队列时我们发送参数42, IBackgroundManager将实体化并以42为TestJob的参数执行它。
让我们看一下使用上面定义的SimpleSendEmailJob的例子:
[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(或EnqueueAsync) 有其它参数,如priority和delay。
BackgroundJobManager默认实现了IBackgroundJobManager,它可被其它后台作业提供器替代(查看Hangfire文档)。如下为一些关于默认BackgroudJobManager的信息:
- 它是一个简单的作业队列,以FIFO(先进先出)方式单线程作业,它使用IBackgroundJobStore来持久化作业(见下一小节)。
- 它一直重试作业执行直到作业成功运行(只记录日志不抛出异常)或超时,默认超时为一个作业2天。
- 在作业成功运行后,它从存储(数据库)里删除这个作业,如果超时了,就把这个作业设置为“被抛弃的”,然后离开数据库。
- 它在重试一个作业之间递增等待时间,第一次重试,等待1分钟,第二次重试,等待2分钟,第三次重试,等待4分钟,如此类推。
- 它在固定的间隔里给作业的存储投票,查询作业按优先级(升序)排序,然后按尝试次数(升序)排。
默认的BackgroundJobManager需要一个数据存储来保存和获取作业,如果你没有实现IBackgroundJobStore,它会使用InMemoryBackgroundJobStore,它不在持久化的数据库中保存作业,你可以简单的实现这个接口,让作业存储到一个数据库或使用已经实现该接口的module-zero。
如果你使用第三方的作业管理器(如Hangfire),不需要实现IBackgroundJobStore。
你可以在模块的PreInitialize方法里,使用Configuration.BackgroundJobs配置你的后台作业系统。
你可能会想为你的应用禁用后台作业执行:
public class MyProjectWebModule : AbpModule { public override void PreInitialize() { Configuration.BackgroundJobs.IsJobExecutionEnabled = false; } //... }
很少需要这样,但考虑一下你正在运行一个应用的多个实例并访问同一个数据库,这种情况下,每个应用将向同个数据库查询作业并执行它们,这可能导致同个作业的多次执行和一些其它问题,为阻止这种情况,我们有两个选择:
- 你可以只允许应用的一个实例来完成作业的执行。
- 你可以禁用应用的所有实例执行作业,再单独创建一个应用(如:一个windows服务)来执行后台作业。
后台作业管理器设计成可被其它后台管理器所替换,查看Hangfire集成文档如何用Hangfire代替。
后台工作者与后台作业不同,它简单的依赖应用在后台运行的线程,通常地,它定期执行一些任务,例如:
- 一个后台工作者可以定期删除旧日志。
- 一个后台工作者可以定期检测不活跃的用户,然后发邮件给他们,让他们重新使用你的应用。
为创建一个后台工作者,我们应当实现IBackgroundWorker接口,我们还可以选择直接从BackgroundWorkerBase或PeriodicBackgroundWorkerBase基类上继承。
假设我们想把超过30天未登录的用户设置为“消极”的,代码如下:
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(); } } }
这是一段真实的代码,它工作在ABP的module-zero里。
- 如果你从PeriodicBackgroundWorkerBase继承(如这个例子),需要实现DoWork方法来执行你的定期工作。
- 如果你从BackgroundWorkerBase继承或直接实现IBackgroundWorker,需要重写/实现Start、Stop和WaitToStop方法,Start和Stop方法应当是非阻塞的,WaitToStop方法需要等待工作者完成它当前的工作。
在完成创建后台工作者后,需要把它添加到IBackgroundWorkerManager,非常通用的地方是:你模块的PostInitialize方法里:
public class MyProjectWebModule : AbpModule { //... public override void PostInitialize() { var workManager = IocManager.Resolve<IBackgroundWorkerManager>(); workManager.Add(IocManager.Resolve<MakeInactiveUsersPassiveWorker>()); } }
虽然我们通常在PostInitialize里添加工作者,但不是一定要这样,你可以在任何地方注入IBackgroundWorkerManager,然后在运行时添加工作者。
当你应用关闭时,IBackgroundWorkerManager将停止并释放所有已注册的工作者。
后台工作者以单例模式被创建,但也不是一定要这样,如果你需要同个工作者类的多个实例,你可以使它是“暂时的”并添加多个实例到IBackgroundWorkermanager,在这种情况下,你的工作者可能需要参数(假设你有一个单独的LogCleaner类,但有两个LogCleaner工作者实例用来监视和删除不同的日志目录)。
只有当你的应用运行时,后台作业和工作者才能工作,如果一个Web应用长时间没有收到访问请求,它默认地会被关闭,所以,如果你的宿主后台作业运行在你的web应用里(这是默认行为),你应当确保你的web应用被配置成一直运行,否则,只有当你的应用在使用的时候,后台作业才能工作。
这里有些技术可以做到这点,一个非常简单的办法是:从一个外部应用里定期访问你的Web应用,从而你可以一直检查你的web应用是否一直运行着。Hangfire文档解释了一些其它方法。
kid1412附:英文原文:http://www.aspnetboilerplate.com/Pages/Documents/Background-Jobs-And-Workers