项目

后台作业

引言

后台作业用于将某些任务加入队列,在后台执行。使用后台作业的原因有多种,以下是一些示例:

  • 执行长时间运行的任务而无需用户等待。例如,用户点击“报告”按钮启动一个耗时的报告生成任务。您可以将此任务加入队列,并在完成后通过电子邮件将报告结果发送给用户。
  • 创建可重试持久化任务,以保证代码能够成功执行。例如,您可以通过后台作业发送电子邮件,以克服临时故障确保邮件最终被发送。这样用户无需等待邮件发送过程。

后台作业具有持久性,这意味着即使应用程序崩溃,它们也会被重试并在之后执行

抽象包

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; }
    }
}
自定义作业名称

您可以配置 AbpBackgroundJobOptionsGetBackgroundJobName 委托来更改默认作业名称。

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 枚举,包含 LowBelowNormalNormal(默认)、AboveNormalHight 字段。
  • 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.BackgroundJobs nuget包包含默认的后台作业管理器,默认已安装到启动模板中。

配置

在您的模块类中使用 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 中设置 DefaultQueueNamePrefixDefaultDelayedQueueNamePrefix 属性为您的应用程序名称:

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或磁盘),这可能是一个不错的选择,因此您可以将该后台应用程序部署到专用服务器,并且后台作业不会影响应用程序的性能。

集成

后台作业系统是可扩展的,您可以用自己的实现或预构建的集成之一更改默认后台作业管理器。

参见预构建的作业管理器替代方案:

另请参阅

在本文档中