## 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)中描述了另外的实现方法。
- 文章&教程
- Abp
- 1.1ABP总体介绍-入门介绍
- 1.2ABP总体介绍-多层架构体系
- 1.3ABP总体介绍-模块系统
- 1.4ABP总体介绍-启动配置
- 1.5ABP总体介绍-多租户
- 1.6ABP总体介绍-集成OWIN
- 2.1ABP公共结构-依赖注入
- 2.2ABP公共结构-会话管理
- 2.3ABP公共结构-缓存管理
- 2.4ABP公共结构-日志管理
- 2.5ABP公共结构-设置管理
- 2.6ABP公共结构-时区设置
- 3.1ABP领域层-实体
- 3.2ABP领域层-值对象
- 3.3ABP领域层-仓储
- 3.4ABP领域层-领域服务
- 3.5ABP领域层-工作单元
- 3.6ABP领域层-领域事件
- 3.7ABP领域层-数据过滤器
- 4.1ABP应用层-应用服务
- 4.2ABP应用层-数据传输对象
- 4.3ABP应用层-数据传输对象验证
- 4.4ABP应用层-权限认证
- 4.5ABP应用层-功能管理
- 4.6ABP应用层-审计日志
- 5.1ABP分布式服务-ASP.NET WebApi
- 5.2ABP分布式服务-动态WebApi层
- 5.3ABP分布式服务-集成OData
- 5.4ABP分布式服务-集成SwaggerUI
- 6.1ABP表现层-Mvc控制器
- 6.2ABP表现层-Mvc视图
- 6.3ABP表现层-本地化
- 6.4ABP表现层-导航栏
- 6.5ABP表现层-异常处理
- 6.6.2-AJAX
- 6.6.3-Notification
- 6.6.4-Message
- 6.6.5-UIBlockBusy
- 6.6.6-EventBus
- 6.6.7-Logging
- 6.6.8-OtherUtilities
- 6.6ABP表现层-JavascriptAPI
- 6.7ABP表现层-嵌入资源文件
- 6.8ASP.NET-Core
- 6.9CSRF和XSRF保护
- 7.1ABP后台服务-后台作业和工人
- 7.2ABP后台服务-集成Hangfire
- 8.1ABP实时服务-通知系统
- 8.2ABP实时服务-集成SignalR
- 9.1ABP基础设施层-集成Entity Framework
- 9.2ABP基础设施层-集成NHibernate
- 9.3ABP基础设施层-集成Entity Framework Core
- abp合并版160929
- md文档排版规范
- AbpZero
- 1.1ABPZero-概述
- 1.2ABPZero-安装
- 1.3ABPZero-启动模板
- 1.4ABPZero-启动模板Core
- 2.1ABPZero-多租户管理
- 2.2ABPZero-版本管理
- 2.4ABPZero-组织单位管理