MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C#后台服务(BackgroundService)与Quartz.NET调度

2021-10-237.3k 阅读

C# 后台服务(BackgroundService)

1. BackgroundService 基础概念

在 C# 开发中,BackgroundService 是.NET 提供的一个抽象类,用于创建长时间运行的后台服务。它在 Microsoft.Extensions.Hosting 命名空间下,是构建基于主机的应用程序中后台任务的一种便捷方式。

BackgroundService 类为我们提供了一个抽象方法 ExecuteAsync(CancellationToken stoppingToken),我们需要在继承自 BackgroundService 的自定义类中实现这个方法,在这个方法中编写我们后台任务的具体逻辑。CancellationToken stoppingToken 用于接收取消信号,以便我们在收到停止服务的请求时,能够优雅地停止后台任务。

2. 创建简单的 BackgroundService 示例

首先,创建一个新的.NET 控制台应用程序项目。假设项目名为 MyBackgroundServiceApp

在项目中安装 Microsoft.Extensions.Hosting 包,这是使用 BackgroundService 所必需的。可以通过 NuGet 包管理器控制台执行以下命令来安装:

Install-Package Microsoft.Extensions.Hosting

接下来,创建一个继承自 BackgroundService 的类,例如 MyBackgroundService

using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using System.Threading.Tasks;

public class MyBackgroundService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            Console.WriteLine("Background service is running...");
            await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
        }
    }
}

在上述代码中,ExecuteAsync 方法内部使用一个 while 循环来持续运行任务,只要 stoppingToken 没有收到取消请求。每 5 秒钟,服务会在控制台输出一条消息。

然后,在 Program.cs 文件中配置并启动这个后台服务:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;

class Program
{
    static async Task Main()
    {
        using var host = Host.CreateDefaultBuilder()
           .ConfigureServices((hostContext, services) =>
            {
                services.AddHostedService<MyBackgroundService>();
            })
           .Build();

        await host.RunAsync();
    }
}

Main 方法中,通过 Host.CreateDefaultBuilder() 创建一个默认的主机生成器。在 ConfigureServices 方法中,使用 services.AddHostedService<MyBackgroundService>() 将我们自定义的后台服务注册到服务集合中。最后,调用 host.RunAsync() 来启动主机,从而运行我们的后台服务。

3. BackgroundService 的生命周期管理

BackgroundService 的生命周期与主机应用程序紧密相关。当主机启动时,后台服务的 ExecuteAsync 方法会被调用,开始执行后台任务。当主机收到停止信号(例如通过操作系统的服务控制命令或者调用 CancellationTokenSource.Cancel 方法)时,stoppingToken 会收到取消请求,ExecuteAsync 方法中的循环会结束,后台任务可以进行一些清理操作后结束。

此外,BackgroundService 类还提供了一些虚拟方法,如 StartAsync(CancellationToken cancellationToken)StopAsync(CancellationToken cancellationToken),可以在需要时重写它们来进行更复杂的启动和停止逻辑处理。例如,在 StartAsync 方法中可以进行一些初始化操作,在 StopAsync 方法中可以进行资源释放等操作。

4. 与依赖注入(DI)的集成

BackgroundService 可以很好地与依赖注入(DI)集成。在注册后台服务时,可以将需要的依赖项注入到后台服务类的构造函数中。

假设我们有一个简单的接口 IMyService 和它的实现类 MyService

public interface IMyService
{
    void DoWork();
}

public class MyService : IMyService
{
    public void DoWork()
    {
        Console.WriteLine("MyService is doing work.");
    }
}

然后,修改 MyBackgroundService 类,使其依赖 IMyService

public class MyBackgroundService : BackgroundService
{
    private readonly IMyService _myService;

    public MyBackgroundService(IMyService myService)
    {
        _myService = myService;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _myService.DoWork();
            await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
        }
    }
}

Program.cs 中注册 IMyServiceMyBackgroundService

class Program
{
    static async Task Main()
    {
        using var host = Host.CreateDefaultBuilder()
           .ConfigureServices((hostContext, services) =>
            {
                services.AddSingleton<IMyService, MyService>();
                services.AddHostedService<MyBackgroundService>();
            })
           .Build();

        await host.RunAsync();
    }
}

这样,在 MyBackgroundServiceExecuteAsync 方法中就可以使用注入的 IMyService 实例来执行相关的业务逻辑。

Quartz.NET 调度

1. Quartz.NET 概述

Quartz.NET 是一个功能强大的开源作业调度框架,用于在.NET 应用程序中执行定时任务。它允许我们定义作业(Job),这些作业可以在指定的时间间隔或特定的时间点执行。Quartz.NET 提供了丰富的调度选项,包括简单的时间间隔调度、基于日历的调度等。

2. Quartz.NET 核心概念

  • 作业(Job):作业是要执行的实际工作单元。在 Quartz.NET 中,我们通过创建一个实现 IJob 接口的类来定义作业。IJob 接口只有一个方法 Execute(IJobExecutionContext context),在这个方法中编写作业的具体逻辑。
  • 触发器(Trigger):触发器用于定义作业何时执行。Quartz.NET 提供了多种类型的触发器,如简单触发器(SimpleTrigger)用于在指定的时间间隔内重复执行作业,以及日历触发器(CronTrigger)用于基于日历表达式进行调度。
  • 调度器(Scheduler):调度器是 Quartz.NET 的核心组件,它负责管理作业和触发器,并根据触发器的定义来调度作业的执行。

3. 创建简单的 Quartz.NET 示例

首先,创建一个新的.NET 控制台应用程序项目,假设项目名为 MyQuartzApp

安装 Quartz.NET 相关的 NuGet 包。可以通过 NuGet 包管理器控制台执行以下命令:

Install-Package Quartz
Install-Package Quartz.Spi
Install-Package Quartz.Impl

创建一个实现 IJob 接口的作业类,例如 MyJob

using Quartz;
using System;
using System.Threading.Tasks;

public class MyJob : IJob
{
    public async Task Execute(IJobExecutionContext context)
    {
        Console.WriteLine("MyJob is executing at {0}", DateTime.Now);
        await Task.CompletedTask;
    }
}

在上述代码中,MyJobExecute 方法简单地在控制台输出当前时间,表示作业正在执行。

接下来,在 Program.cs 文件中配置并启动 Quartz.NET 调度器:

using Quartz;
using Quartz.Impl;
using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        // 创建调度器工厂
        var schedulerFactory = new StdSchedulerFactory();
        // 获取调度器实例
        var scheduler = await schedulerFactory.GetScheduler();

        // 创建作业详情
        var job = JobBuilder.Create<MyJob>()
           .WithIdentity("myJob", "group1")
           .Build();

        // 创建触发器
        var trigger = TriggerBuilder.Create()
           .WithIdentity("myTrigger", "group1")
           .StartNow()
           .WithSimpleSchedule(x => x
              .WithIntervalInSeconds(5)
              .RepeatForever())
           .Build();

        // 将作业和触发器添加到调度器
        await scheduler.ScheduleJob(job, trigger);

        // 启动调度器
        await scheduler.Start();

        // 等待一段时间,以便作业执行
        await Task.Delay(TimeSpan.FromSeconds(30));

        // 停止调度器
        await scheduler.Shutdown();
    }
}

在上述代码中,首先通过 StdSchedulerFactory 创建调度器实例。然后,使用 JobBuilder 创建作业详情,给作业指定一个唯一标识。接着,使用 TriggerBuilder 创建一个简单触发器,该触发器从当前时间开始,每 5 秒钟触发一次作业执行。将作业和触发器添加到调度器后,启动调度器开始作业调度。程序等待 30 秒后停止调度器。

4. 使用 CronTrigger 进行复杂调度

CronTrigger 允许我们使用 Cron 表达式进行复杂的时间调度。Cron 表达式是一个字符串,由 6 或 7 个空格分隔的字段组成,分别表示秒、分钟、小时、日期、月份、星期几(可选)和年份(可选)。

以下是一个使用 CronTrigger 的示例,假设我们希望作业每天凌晨 2 点执行:

using Quartz;
using Quartz.Impl;
using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        var schedulerFactory = new StdSchedulerFactory();
        var scheduler = await schedulerFactory.GetScheduler();

        var job = JobBuilder.Create<MyJob>()
           .WithIdentity("myJob", "group1")
           .Build();

        var trigger = TriggerBuilder.Create()
           .WithIdentity("myTrigger", "group1")
           .WithCronSchedule("0 0 2 * *?")
           .Build();

        await scheduler.ScheduleJob(job, trigger);
        await scheduler.Start();

        // 等待一段时间,这里只是示例,实际应用中可以根据需求调整
        await Task.Delay(TimeSpan.FromSeconds(10));

        await scheduler.Shutdown();
    }
}

在上述代码中,WithCronSchedule("0 0 2 * *?") 表示每天凌晨 2 点执行作业。Cron 表达式的每个字段含义如下:

  • 第一个字段 0 表示秒,这里是 0 秒。
  • 第二个字段 0 表示分钟,这里是 0 分钟。
  • 第三个字段 2 表示小时,这里是 2 点。
  • 第四个字段 * 表示日期可以是任何值。
  • 第五个字段 * 表示月份可以是任何值。
  • 第六个字段 ? 表示星期几不指定,因为我们是基于日期(每天凌晨 2 点)调度,而不是基于星期几。

5. Quartz.NET 与依赖注入集成

BackgroundService 类似,Quartz.NET 也可以与依赖注入集成。在使用 Quartz.NET 时,可以通过自定义的 IJobFactory 来实现依赖注入到作业类中。

首先,创建一个自定义的 IJobFactory 实现类,例如 MyJobFactory

using Quartz;
using Quartz.Spi;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

public class MyJobFactory : IJobFactory
{
    private readonly IServiceProvider _serviceProvider;

    public MyJobFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
    {
        return _serviceProvider.GetService(bundle.JobDetail.JobType) as IJob;
    }

    public void ReturnJob(IJob job)
    {
        // 如果需要,可以在这里进行资源回收等操作
    }
}

在上述代码中,MyJobFactory 的构造函数接收一个 IServiceProvider 实例,在 NewJob 方法中,通过 _serviceProvider.GetService 方法从服务容器中获取作业实例,从而实现依赖注入。

然后,在 Program.cs 中注册 MyJobFactory 并使用它来创建调度器:

using Quartz;
using Quartz.Impl;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        var services = new ServiceCollection();
        services.AddSingleton<IMyService, MyService>();
        var serviceProvider = services.BuildServiceProvider();

        var schedulerFactory = new StdSchedulerFactory();
        var scheduler = await schedulerFactory.GetScheduler();

        scheduler.JobFactory = new MyJobFactory(serviceProvider);

        var job = JobBuilder.Create<MyJob>()
           .WithIdentity("myJob", "group1")
           .Build();

        var trigger = TriggerBuilder.Create()
           .WithIdentity("myTrigger", "group1")
           .StartNow()
           .WithSimpleSchedule(x => x
              .WithIntervalInSeconds(5)
              .RepeatForever())
           .Build();

        await scheduler.ScheduleJob(job, trigger);
        await scheduler.Start();

        await Task.Delay(TimeSpan.FromSeconds(30));

        await scheduler.Shutdown();
    }
}

在上述代码中,首先通过 ServiceCollection 创建一个服务集合,并注册 IMyService。然后创建 MyJobFactory 实例并将其设置为调度器的 JobFactory。这样,当作业实例被创建时,就可以从服务容器中获取依赖项进行注入。

C# 后台服务(BackgroundService)与 Quartz.NET 调度的结合使用

1. 结合的优势

BackgroundService 与 Quartz.NET 调度结合使用,可以充分发挥两者的优势。BackgroundService 提供了一个与.NET 主机集成的便捷方式来管理后台任务的生命周期,而 Quartz.NET 提供了强大的作业调度功能。通过结合使用,可以实现更灵活、可靠的后台任务调度。

例如,在一个企业级应用程序中,可能有一些定时任务需要在后台持续运行,如定期的数据备份、数据同步等。使用 BackgroundService 可以将这些任务作为主机应用程序的一部分进行管理,而 Quartz.NET 可以精确地控制这些任务的执行时间和频率。

2. 结合使用示例

假设我们有一个应用程序,需要在后台定期执行数据备份任务。首先,创建一个继承自 BackgroundService 的类 BackupService

using Microsoft.Extensions.Hosting;
using Quartz;
using Quartz.Impl;
using System;
using System.Threading;
using System.Threading.Tasks;

public class BackupService : BackgroundService
{
    private readonly IScheduler _scheduler;

    public BackupService()
    {
        var schedulerFactory = new StdSchedulerFactory();
        _scheduler = schedulerFactory.GetScheduler().Result;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var job = JobBuilder.Create<BackupJob>()
           .WithIdentity("backupJob", "group1")
           .Build();

        var trigger = TriggerBuilder.Create()
           .WithIdentity("backupTrigger", "group1")
           .WithCronSchedule("0 0 2 * *?")
           .Build();

        await _scheduler.ScheduleJob(job, trigger);
        await _scheduler.Start();

        while (!stoppingToken.IsCancellationRequested)
        {
            await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
        }

        await _scheduler.Shutdown();
    }
}

在上述代码中,BackupService 的构造函数创建了一个 Quartz.NET 的调度器实例。在 ExecuteAsync 方法中,定义了一个数据备份作业 BackupJob(后续会创建)和一个每天凌晨 2 点执行的 CronTrigger。启动调度器后,通过一个 while 循环等待取消信号,当收到取消信号时,停止调度器。

接下来,创建 BackupJob 类:

using Quartz;
using System;
using System.Threading.Tasks;

public class BackupJob : IJob
{
    public async Task Execute(IJobExecutionContext context)
    {
        Console.WriteLine("Data backup is running at {0}", DateTime.Now);
        // 这里可以编写实际的数据备份逻辑,例如数据库备份等
        await Task.CompletedTask;
    }
}

BackupJob 类实现了 IJob 接口,在 Execute 方法中简单地输出当前时间表示数据备份任务正在执行,实际应用中可以在这里编写具体的数据备份代码,如使用数据库连接进行数据导出等操作。

最后,在 Program.cs 文件中配置并启动 BackupService

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;

class Program
{
    static async Task Main()
    {
        using var host = Host.CreateDefaultBuilder()
           .ConfigureServices((hostContext, services) =>
            {
                services.AddHostedService<BackupService>();
            })
           .Build();

        await host.RunAsync();
    }
}

通过上述配置,BackupService 作为一个后台服务被主机管理,其中使用 Quartz.NET 调度器来定时执行数据备份作业。

3. 异常处理与监控

在结合使用 BackgroundService 和 Quartz.NET 时,异常处理和监控是非常重要的。

对于 BackgroundService,可以在 ExecuteAsync 方法中使用 try - catch 块来捕获后台任务执行过程中的异常。例如:

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    try
    {
        var job = JobBuilder.Create<BackupJob>()
           .WithIdentity("backupJob", "group1")
           .Build();

        var trigger = TriggerBuilder.Create()
           .WithIdentity("backupTrigger", "group1")
           .WithCronSchedule("0 0 2 * *?")
           .Build();

        await _scheduler.ScheduleJob(job, trigger);
        await _scheduler.Start();

        while (!stoppingToken.IsCancellationRequested)
        {
            await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
        }

        await _scheduler.Shutdown();
    }
    catch (Exception ex)
    {
        // 记录异常日志
        Console.WriteLine($"An error occurred in BackupService: {ex.Message}");
    }
}

在上述代码中,捕获到异常后,将异常信息输出到控制台,实际应用中可以使用专业的日志框架(如 Serilog)来记录异常日志。

对于 Quartz.NET,作业执行过程中的异常可以在作业类的 Execute 方法中进行捕获处理。例如:

public async Task Execute(IJobExecutionContext context)
{
    try
    {
        Console.WriteLine("Data backup is running at {0}", DateTime.Now);
        // 这里可以编写实际的数据备份逻辑,例如数据库备份等
        await Task.CompletedTask;
    }
    catch (Exception ex)
    {
        // 记录异常日志
        Console.WriteLine($"An error occurred in BackupJob: {ex.Message}");
    }
}

此外,还可以使用 Quartz.NET 提供的监听器(Listener)来监控作业和触发器的执行情况。例如,可以创建一个作业监听器来监听作业的执行开始和结束:

using Quartz;
using System;
using System.Threading.Tasks;

public class BackupJobListener : IJobListener
{
    public string Name => "BackupJobListener";

    public async Task JobToBeExecuted(IJobExecutionContext context)
    {
        Console.WriteLine("Backup job is about to execute at {0}", DateTime.Now);
        await Task.CompletedTask;
    }

    public async Task JobExecutionVetoed(IJobExecutionContext context)
    {
        Console.WriteLine("Backup job execution was vetoed at {0}", DateTime.Now);
        await Task.CompletedTask;
    }

    public async Task JobWasExecuted(IJobExecutionContext context, JobExecutionException jobException)
    {
        if (jobException != null)
        {
            Console.WriteLine($"Backup job execution failed: {jobException.Message}");
        }
        else
        {
            Console.WriteLine("Backup job executed successfully at {0}", DateTime.Now);
        }
        await Task.CompletedTask;
    }
}

然后,在 BackupServiceExecuteAsync 方法中注册这个监听器:

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    try
    {
        var job = JobBuilder.Create<BackupJob>()
           .WithIdentity("backupJob", "group1")
           .Build();

        var trigger = TriggerBuilder.Create()
           .WithIdentity("backupTrigger", "group1")
           .WithCronSchedule("0 0 2 * *?")
           .Build();

        var listener = new BackupJobListener();
        await _scheduler.ListenerManager.AddJobListener(listener, GroupMatcher<JobKey>.GroupEquals("group1"));

        await _scheduler.ScheduleJob(job, trigger);
        await _scheduler.Start();

        while (!stoppingToken.IsCancellationRequested)
        {
            await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
        }

        await _scheduler.Shutdown();
    }
    catch (Exception ex)
    {
        // 记录异常日志
        Console.WriteLine($"An error occurred in BackupService: {ex.Message}");
    }
}

通过上述异常处理和监控机制,可以提高后台服务和调度任务的可靠性和可维护性。

4. 动态调度与配置

在实际应用中,可能需要根据运行时的配置动态地调整调度任务。例如,根据用户在配置文件中设置的备份时间来动态创建触发器。

可以在 BackupService 类中添加一个方法来根据配置创建触发器:

private ITrigger CreateTriggerFromConfig(string cronExpression)
{
    return TriggerBuilder.Create()
       .WithIdentity("backupTrigger", "group1")
       .WithCronSchedule(cronExpression)
       .Build();
}

然后,在 ExecuteAsync 方法中从配置文件中读取 Cron 表达式并创建触发器:

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    try
    {
        var job = JobBuilder.Create<BackupJob>()
           .WithIdentity("backupJob", "group1")
           .Build();

        // 从配置文件中读取Cron表达式,这里假设使用ConfigurationManager读取配置
        var cronExpression = ConfigurationManager.AppSettings["BackupCronExpression"];
        var trigger = CreateTriggerFromConfig(cronExpression);

        await _scheduler.ScheduleJob(job, trigger);
        await _scheduler.Start();

        while (!stoppingToken.IsCancellationRequested)
        {
            await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
        }

        await _scheduler.Shutdown();
    }
    catch (Exception ex)
    {
        // 记录异常日志
        Console.WriteLine($"An error occurred in BackupService: {ex.Message}");
    }
}

在配置文件(如 app.configappsettings.json)中添加备份时间的配置:

<appSettings>
    <add key="BackupCronExpression" value="0 0 3 * *?" />
</appSettings>

通过这种方式,可以根据运行时的配置动态调整调度任务的执行时间,提高应用程序的灵活性。

通过以上对 C# 后台服务(BackgroundService)与 Quartz.NET 调度的详细介绍,包括各自的基础概念、使用示例、结合使用方法以及异常处理和动态配置等方面,希望能帮助开发者在实际项目中更好地实现后台任务的调度和管理。无论是简单的定时任务还是复杂的企业级调度需求,都可以通过合理运用这些技术来高效地完成。