Welcome to visit tcdos.com
...

by Zengjing 2021-06-12

.NET Core集成Quartz实现定时任务管理

在.NET Core中可以通过BackgroundService来实现简单的定时任务服务,一旦涉及到生产环境下的具体业务,就显得力不从心了。这时,我们可以考虑集成作业调度框架Quartz来实现定时服务管理。

Quartz.NET官网:https://www.quartz-scheduler.net/

本文基于.NET Core SDK5.0 和 Quartz3.3.2,最新版本可能有差异,仅供参考。

目标
  • 任务基本管理:包括新增、编辑和删除任务。
  • 任务操作管理:包括自启动、启动、停止任务。
Quartz

Quartz 由三个部分组成,分别为Schedule(调度器)、Trigger(触发器)、Job(任务)。其中,Job是指被调度的任务;Trigger是控制任务运行的触发器;Schedule是负责协调Job和Trigger的独立容器。

IJob

继承与实现IJob接口,执行任务并记录任务日志。

public class HttpJob : IJob
{
    private readonly ILogger<HttpJob> logger;
    private readonly IHttpClientFactory httpClientFactory;

    public HttpJob(ILogger<HttpJob> logger, IHttpClientFactory httpClientFactory)
    {
        this.logger = logger;
        this.httpClientFactory = httpClientFactory;
    }

    public async Task Execute(IJobExecutionContext context)
    {
        // 任务: 执行
        var flag = false;
        var model = JsonConvert.DeserializeObject<TaskDto>(context.JobDetail.JobDataMap.GetString("model"));
        string httpMessage = string.Empty;

        if (model != null)
        {
            try
            {
                Dictionary<string, string> header = new();
                if (!string.IsNullOrEmpty(model.AuthKey) && !string.IsNullOrEmpty(model.AuthValue))
                {
                    header.Add(model.AuthKey.Trim(), model.AuthValue.Trim());
                }

                httpMessage = await httpClientFactory.HttpSendAsync(
                    model.ApiMethod?.ToLower() == "get" ? HttpMethod.Get : HttpMethod.Post,
                    model.ApiUrl,
                    header);

                logger.LogInformation($"任务: {context.JobDetail.Key.Group}.{context.JobDetail.Key.Name} 执行成功");
                logger.LogInformation(httpMessage);

                flag = true;
            }
            catch (Exception ex)
            {
                logger.LogError($"任务: {context.JobDetail.Key.Group}.{context.JobDetail.Key.Name} 执行失败");
                logger.LogError(ex.Message);
            }

            // 任务: 执行日志 
            try
            {
                using (var efDbContext = (EfDbContext)ServiceLocator.Instance.CreateScope().ServiceProvider.GetService(typeof(EfDbContext)))
                {
                    var guid = ZnResponse.GuId();
                    var remark = flag ? httpMessage : "执行失败";
                    // 日志
                    efDbContext.Database.ExecuteSqlRaw("INSERT INTO QTZLogger (ID,TASKID,EXECUTEDDATE,REMARKS) VALUES ({0},{1},{2},{3})", guid, model?.TaskId, DateTime.Now, remark);
                    // 更新
                    efDbContext.Database.ExecuteSqlRaw("UPDATE QTZTask SET LASTRUNTIME={0} WHERE TASKID={1}", DateTime.Now, model?.TaskId);
                }
                logger.LogInformation("任务: 日志入库成功");
            }
            catch (Exception ex)
            {
                logger.LogError($"任务: 日志入库失败");
                logger.LogError(ex.Message);
            }
            /*
            try
            {
                //并发线程不安全导致入库失败 see https://go.microsoft.com/fwlink/?linkid=2097913

                var guid = ZnResponse.GuId();
                var remark = flag ? "执行成功" : "执行失败";
                EfDbContext efDbContext = (EfDbContext)ServiceLocator.Instance.GetService(typeof(EfDbContext));
                efDbContext.Database.ExecuteSqlRaw("INSERT INTO QTZLogger (ID,TASKID,EXECUTEDDATE,REMARKS) VALUES ({0},{1},{2},{3})", guid, model.TaskId, DateTime.Now, remark);

                NLogHelp.Info("任务: 日志入库成功");
            }
            catch (Exception ex)
            {
                NLogHelp.Error($"任务: 日志入库失败");
                NLogHelp.Error(ex.Message);
            }
            */
        }
        else
        {
            logger.LogError($"任务: {context.JobDetail.Key.Group}.{context.JobDetail.Key.Name} 执行异常(JobDataMap is null)");
        }
    }
}
IJobFactory

继承与实现IJobFactory,调度执行任务。

public class JobFactory : IJobFactory
{
    private readonly IServiceProvider _serviceProvider;

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

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

    public void ReturnJob(IJob job)
    {
        var disposable = job as IDisposable;
        disposable?.Dispose();
    }
}
Trigger

封装创建触发器与创建作业实例静态方法。

public static class QuartzExtension
{
    /// <summary>
    /// 创建触发器
    /// </summary>
    public static ITrigger CreateCronTrigger(TaskDto task)
    {
        DateTimeOffset beginTime = DateBuilder.NextGivenSecondDate(task.BeginTime, 1);
        DateTimeOffset endTime = DateBuilder.NextGivenSecondDate(task.EndTime, 1);

        return TriggerBuilder.Create()
                .WithIdentity(task.TaskName, task.GroupName)
                .StartAt(beginTime)
                .EndAt(endTime)
                .WithCronSchedule(task.Interval)
                .ForJob(task.TaskName, task.GroupName)
                .Build();
    }

    /// <summary>
    /// 创建作业实例
    /// </summary>
    public static IJobDetail CreateJob(TaskDto task)
    {
        return JobBuilder
            .Create<HttpJob>()
            .WithIdentity(task.TaskName, task.GroupName)
            .WithDescription(task.TaskDesc)
            .UsingJobData("model", JsonConvert.SerializeObject(task))
            .Build();
    }
}
注入集成

在startup中注入集成Quartz。

services.AddSingleton<ISchedulerFactory, StdSchedulerFactory>();
services.AddSingleton<IJobFactory, JobFactory>();
services.AddSingleton<HttpJob>();

然后在Configure实现任务自启动。

app.UseQuartzInit();
/// <summary>
/// 程序启动将任务调度表里所有状态为 执行中 任务启动起来
/// </summary>
public static IApplicationBuilder UseQuartzInit(this IApplicationBuilder app)
{
    if (app == null) throw new ArgumentNullException(nameof(app));

    var flag = Appsettings.Get(new string[] { "QTZConfig", "Enabled" }).ObjToBool();
    if (flag)
    {

        IServiceProvider services = app.ApplicationServices;
        EfDbContext efDbContext = services.GetService<EfDbContext>();
        ISchedulerFactory schedulerFactory = services.GetService<ISchedulerFactory>();
        IJobFactory jobFactory = services.GetService<IJobFactory>();

        if (efDbContext != null)
        {
            NLogHelp.Info($"任务管理: 初始化");
            int i = 0;
            var taskList = efDbContext.QTZTask.Where(x => x.Status == 1).ToList();

            // 通过工场类获得调度器
            IScheduler scheduler = schedulerFactory.GetScheduler().GetAwaiter().GetResult();
            scheduler.JobFactory = jobFactory;

            // 装载作业任务
            taskList.ForEach(async x =>
            {
                try
                {
                    var jk = new JobKey(x.TaskName, x.GroupName);
                    if (await scheduler.CheckExists(jk))
                    {
                        await scheduler.PauseJob(jk);
                        await scheduler.DeleteJob(jk);
                    }

                    if (!string.IsNullOrEmpty(x.Interval) && CronExpression.IsValidExpression(x.Interval))
                    {
                        IJobDetail job = QuartzExtension.CreateJob(x); ;
                        ITrigger trigger = QuartzExtension.CreateCronTrigger(x);

                        // 设置监听器
                        JobListener listener = new();
                        scheduler.ListenerManager.AddJobListener(listener, GroupMatcher<JobKey>.AnyGroup());

                        // 将触发器和作业任务绑定到调度器中
                        scheduler.ScheduleJob(job, trigger).GetAwaiter().GetResult();

                        i++;
                        NLogHelp.Info($"任务管理: 运行成功 - {x.GroupName}.{x.TaskName}");
                    }
                }
                catch (Exception ex)
                {
                    NLogHelp.Error($"任务管理: 运行失败 - {x.GroupName}.{x.TaskName}");
                    NLogHelp.Error(ex.ToString());
                }
            });

            // 开启调度器
            scheduler.Start().GetAwaiter().GetResult();
            NLogHelp.Info($"任务管理: 初始化完成 - 共成功运行 {i} 个任务");
        }
    }
    return app;
}

收获点赞: 0

评论

...