后台作业
引言
后台作业用于将某些任务加入队列,在后台执行。使用后台作业的原因有多种,以下是一些示例:
- 执行长时间运行的任务而无需用户等待。例如,用户点击“报告”按钮启动一个耗时的报告生成任务。您可以将此任务加入队列,并在完成后通过电子邮件将报告结果发送给用户。
- 创建可重试和持久化任务,以保证代码能够成功执行。例如,您可以通过后台作业发送电子邮件,以克服临时故障并确保邮件最终被发送。这样用户无需等待邮件发送过程。
后台作业具有持久性,这意味着即使应用程序崩溃,它们也会被重试并在之后执行。
抽象包
ABP为后台作业提供了一个抽象模块和多种实现。它包括内置/默认实现,以及Hangfire、RabbitMQ和Quartz的集成。
Volo.Abp.BackgroundJobs.Abstractions NuGet包提供了创建后台作业和将后台作业项加入队列所需的服务。如果您的模块仅依赖此包,它可以独立于实际实现/集成。
Volo.Abp.BackgroundJobs.Abstractions包默认已安装到启动模板中。
创建后台作业
后台作业是一个实现 IBackgroundJob<TArgs> 接口或继承自 BackgroundJob<TArgs> 类的类。TArgs 是一个简单的普通C#类,用于存储作业数据。
以下示例用于在后台发送电子邮件。首先,定义一个类来存储后台作业的参数:
namespace MyProject
{
public class EmailSendingArgs
{
public string EmailAddress { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
}
}
然后创建一个后台作业类,使用 EmailSendingArgs 对象发送电子邮件:
using System.Threading.Tasks;
using Volo.Abp.BackgroundJobs;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Emailing;
namespace MyProject
{
public class EmailSendingJob
: AsyncBackgroundJob<EmailSendingArgs>, ITransientDependency
{
private readonly IEmailSender _emailSender;
public EmailSendingJob(IEmailSender emailSender)
{
_emailSender = emailSender;
}
public override async Task ExecuteAsync(EmailSendingArgs args)
{
await _emailSender.SendAsync(
args.EmailAddress,
args.Subject,
args.Body
);
}
}
}
此作业简单地使用 IEmailSender 发送电子邮件(参见电子邮件发送文档)。
AsyncBackgroundJob用于创建需要执行异步调用的作业。如果方法不需要执行任何异步调用,您可以继承BackgroundJob<TJob>并重写Execute方法。
异常处理
后台作业不应隐藏异常。如果抛出异常,后台作业会在计算出的等待时间后自动重试。仅当您不希望为当前参数重新运行后台作业时,才隐藏异常。
取消后台作业
如果您的后台任务可取消,则可以使用标准的取消令牌系统获取 CancellationToken,以便在请求时取消作业。请参见以下使用 ICancellationTokenProvider 获取取消令牌的示例:
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Threading;
namespace MyProject
{
public class LongRunningJob : AsyncBackgroundJob<LongRunningJobArgs>, ITransientDependency
{
private readonly ICancellationTokenProvider _cancellationTokenProvider;
public LongRunningJob(ICancellationTokenProvider cancellationTokenProvider)
{
_cancellationTokenProvider = cancellationTokenProvider;
}
public override async Task ExecuteAsync(LongRunningJobArgs args)
{
foreach (var id in args.Ids)
{
_cancellationTokenProvider.Token.ThrowIfCancellationRequested();
await ProcessAsync(id); // 为简洁起见省略了代码
}
}
}
}
如果应用程序正在关闭,并且我们不希望后台作业阻塞应用程序,则可能需要取消操作。此示例在请求取消时抛出异常。因此,作业将在应用程序下次启动时重试。如果您不希望这样,只需从
ExecuteAsync方法返回而不抛出任何异常(您可以简单地检查_cancellationTokenProvider.Token.IsCancellationRequested属性)。
作业名称
每个后台作业都有一个名称。作业名称在多个地方使用。例如,RabbitMQ提供程序使用作业名称来确定RabbitMQ队列名称。
作业名称由作业参数类型决定。对于上面的 EmailSendingArgs 示例,作业名称是 MyProject.EmailSendingArgs(包括命名空间的完整名称)。您可以使用 BackgroundJobName 属性设置不同的作业名称。
示例
using Volo.Abp.BackgroundJobs;
namespace MyProject
{
[BackgroundJobName("emails")]
public class EmailSendingArgs
{
public string EmailAddress { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
}
}
自定义作业名称
您可以配置 AbpBackgroundJobOptions 的 GetBackgroundJobName 委托来更改默认作业名称。
Configure<AbpBackgroundJobOptions>(options =>
{
options.GetBackgroundJobName = (jobType) =>
{
if (jobTyep == typeof(EmailSendingArgs))
{
return "emails";
}
return BackgroundJobNameAttribute.GetName(jobType);
};
});
将作业项加入队列
现在,您可以使用 IBackgroundJobManager 服务将电子邮件发送作业加入队列:
public class RegistrationService : ApplicationService
{
private readonly IBackgroundJobManager _backgroundJobManager;
public RegistrationService(IBackgroundJobManager backgroundJobManager)
{
_backgroundJobManager = backgroundJobManager;
}
public async Task RegisterAsync(string userName, string emailAddress, string password)
{
//TODO: 在数据库中创建新用户...
await _backgroundJobManager.EnqueueAsync(
new EmailSendingArgs
{
EmailAddress = emailAddress,
Subject = "您已成功注册!",
Body = "..."
}
);
}
}
只需注入 IBackgroundJobManager 服务并使用其 EnqueueAsync 方法将新作业添加到队列中。
Enqueue方法接受一些可选参数来控制后台作业:
- priority 用于控制作业项的优先级。它接受一个
BackgroundJobPriority枚举,包含Low、BelowNormal、Normal(默认)、AboveNormal和Hight字段。 - delay 用于在第一次尝试之前等待一段时间(
TimeSpan)。
禁用作业执行
您可能希望禁用应用程序的后台作业执行。这通常适用于您希望在另一个进程中执行后台作业,并在当前进程中禁用它的情况。
使用 AbpBackgroundJobOptions 来配置作业执行:
[DependsOn(typeof(AbpBackgroundJobsModule))]
public class MyModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpBackgroundJobOptions>(options =>
{
options.IsJobExecutionEnabled = false; //禁用作业执行
});
}
}
默认后台作业管理器
ABP包含一个简单的 IBackgroundJobManager 实现,该实现:
- 在单线程中按先进先出(FIFO) 方式工作。
- 重试作业执行,直到作业成功运行或超时。作业的默认超时时间为2天。记录所有异常。
- 当作业成功执行时,从存储(数据库)中删除该作业。如果超时,则将其标记为已放弃并保留在数据库中。
- 在重试之间逐渐增加等待时间。第一次重试等待1分钟,第二次重试等待2分钟,第三次重试等待4分钟,依此类推。
- 以固定间隔轮询存储中的作业。它按优先级(升序)和重试次数(升序)排序查询作业。
Volo.Abp.BackgroundJobsnuget包包含默认的后台作业管理器,默认已安装到启动模板中。
配置
在您的模块类中使用 AbpBackgroundJobWorkerOptions 来配置默认后台作业管理器。以下示例更改了后台作业的超时持续时间:
[DependsOn(typeof(AbpBackgroundJobsModule))]
public class MyModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpBackgroundJobWorkerOptions>(options =>
{
options.DefaultTimeout = 864000; //10天(以秒为单位)
});
}
}
JobPollPeriod用于确定两次作业轮询操作之间的间隔。默认为5000毫秒(5秒)。MaxJobFetchCount用于确定单次轮询操作中获取的最大作业数。默认为1000。DefaultFirstWaitDuration用于确定第一次重试前的等待持续时间。默认为60秒。DefaultTimeout用于确定作业的超时持续时间。默认为172800秒(2天)。DefaultWaitFactor用于确定重试之间等待持续时间增加的因素。默认为2.0。DistributedLockName用于确定要使用的分布式锁名称。默认为AbpBackgroundJobWorker。
数据存储
默认后台作业管理器需要一个数据存储来保存和读取作业。它定义了 IBackgroundJobStore 作为存储作业的抽象。
后台作业模块使用各种数据访问提供程序实现 IBackgroundJobStore。请参阅其文档。如果您不想使用此模块,应自行实现 IBackgroundJobStore 接口。
后台作业模块默认已安装到启动模板中,并根据您的ORM/数据访问选择工作。
为后台作业和工作器使用相同的存储
如果多个应用程序共享相同的后台作业和工作器存储(默认、Hangfire、RabbitMQ和Quartz),您应配置提供程序选项以使用应用程序名称进行隔离。
默认后台作业/工作器
在 AbpBackgroundJobWorkerOptions 中设置 ApplicationName 属性为您的应用程序名称:
public override void PreConfigureServices(ServiceConfigurationContext context)
{
PreConfigure<AbpBackgroundJobWorkerOptions>(options =>
{
options.ApplicationName = context.Services.GetApplicationName()!;
});
}
Hangfire后台作业/工作器
在 AbpHangfireOptions 中设置 DefaultQueuePrefix 属性为您的应用程序名称:
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpHangfireOptions>(options =>
{
options.DefaultQueuePrefix = context.Services.GetApplicationName()!;
});
}
Quartz后台作业/工作器
将 quartz.scheduler.instanceName 属性设置为您的应用程序名称:
public override void PreConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
PreConfigure<AbpQuartzOptions>(options =>
{
options.Properties = new NameValueCollection
{
["quartz.scheduler.instanceName"] = context.Services.GetApplicationName(),
["quartz.jobStore.dataSource"] = "BackgroundJobsDemoApp",
["quartz.jobStore.type"] = "Quartz.Impl.AdoJobStore.JobStoreTX, Quartz",
["quartz.jobStore.tablePrefix"] = "QRTZ_",
["quartz.serializer.type"] = "json",
["quartz.dataSource.BackgroundJobsDemoApp.connectionString"] = configuration.GetConnectionString("Default"),
["quartz.dataSource.BackgroundJobsDemoApp.provider"] = "SqlServer",
["quartz.jobStore.driverDelegateType"] = "Quartz.Impl.AdoJobStore.SqlServerDelegate, Quartz",
};
});
}
RabbitMQ后台作业
在 AbpRabbitMqBackgroundJobOptions 中设置 DefaultQueueNamePrefix 和 DefaultDelayedQueueNamePrefix 属性为您的应用程序名称:
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpRabbitMqBackgroundJobOptions>(options =>
{
options.DefaultQueueNamePrefix = context.Services.GetApplicationName()!.EnsureEndsWith('.') + options.DefaultQueueNamePrefix;
options.DefaultDelayedQueueNamePrefix = context.Services.GetApplicationName()!.EnsureEndsWith('.') + options.DefaultDelayedQueueNamePrefix;
});
}
集群部署
默认后台作业管理器与集群环境兼容(其中多个应用程序实例同时运行)。它使用分布式锁来确保作业一次仅在一个应用程序实例中执行。
但是,分布式锁系统默认在进程内工作。这意味着它实际上不是分布式的,除非您配置分布式锁提供程序。因此,如果尚未配置,请遵循分布式锁文档为您的应用程序配置提供程序。
如果您不想使用分布式锁提供程序,可以选择以下选项:
- 在所有应用程序实例中停止后台作业管理器(如禁用作业执行部分所述,将
AbpBackgroundJobOptions.IsJobExecutionEnabled设置为false),除了其中一个实例,这样只有单个实例执行作业(而其他应用程序实例仍然可以排队作业)。 - 在所有应用程序实例中停止后台作业管理器(如禁用作业执行部分所述,将
AbpBackgroundJobOptions.IsJobExecutionEnabled设置为false),并创建一个专用应用程序(可能是一个在其自己的容器中运行的控制台应用程序,或一个在后台运行的Windows服务)来执行所有后台作业。如果您的后台作业消耗大量系统资源(CPU、RAM或磁盘),这可能是一个不错的选择,因此您可以将该后台应用程序部署到专用服务器,并且后台作业不会影响应用程序的性能。
集成
后台作业系统是可扩展的,您可以用自己的实现或预构建的集成之一更改默认后台作业管理器。
参见预构建的作业管理器替代方案:
抠丁客


