8、quartz.net 支持多点部署的job服务

github地址

https://github.com/quartznet/quartznet

1、概念、作用

quartz是一个job工具,什么是job,可以理解成windows的计划任务。也可以理解成数据库的作业

为什么要用,因为需要用,为什么不用数据库作业去跑,请放过数据库吧

2、安装

因为我是集成在程序里的,所以直接nuget安装库就就可以了

Quartz

Quartz.Serialization.Json(这个必须要,不用问为啥)

3、思路

.net core 基本就是ioc的理念,所以接下来用依赖注入的思路来实现一个小功能,每10秒,输出当前时间到日志内,简约而不简单,因为基本上定时任务就是干这个的

4、开始

4.1 数据库脚本执行

quartz依赖数据库做持久化,支持很多种数据库,可以在下面的网址查看

https://github.com/quartznet/quartznet/tree/master/database/tables

弄下来直接执行就行了

mysql数据库安装配置前面说过了

https://www.cnblogs.com/ares-core/p/12956219.html

 

4.2 配置文件

我们在apollo里,增加一个namespace,然后加入到程序里

Apollo 安装配置请看

https://www.cnblogs.com/ares-core/p/12964701.html

https://www.cnblogs.com/ares-core/p/12975477.html

配置信息如下

{
	"Quartz": {
	  "Enable": true,
	  "JobStoreType": "Quartz.Impl.AdoJobStore.JobStoreTX, Quartz",
	  "JobStoreTablePrefix": "QRTZ_",
	  "DriverDelegateType": "Quartz.Impl.AdoJobStore.MySQLDelegate,Quartz",
	  "JobStoreDataSource": "myDS",
	  "DataSourceProvider": "MySql",
	  "ConnectionString": "Server=192.168.137.220; Port=3306; Database=quartznet; Uid=root; Pwd=123456; persistsecurityinfo=True; CharSet=utf8; SslMode=none;",
	  "Tasks": [{
              "JobName": "GameApiTest",
              "JobGroup": "GameApi",
              "ScheduleTaskType": "GameApi.Web.ScheduleTask.ITest",
              "Type": 3,
              "Cron": "0/10 * * * * ?",
              "Data": "",
              "Description": "测试"
            }]
	}
}

 先别管干什么的,一会儿写代码的时候会挨个说

 

4.3 创建启动服务

干啥用?就是程序启动的时候,根据上面的配置文件,创建任务,当然要判断一下,有没有这个任务,有的话就跳过添加,没有的话就添加

怎么实现?继承 IHostedService 接口即可,IHostedService是啥?去问msdn

 

首先我们创建一个job全局类,原因后面说

using System;
using Quartz;
namespace GameApi.Quartz4Net
{
    public static class JobApplicationContext
    {
        public static IScheduler Scheduler { get; set; }
    }
}

 

然后是两个entity类,用来反序列化Apollo里的参数

using System.Collections.Generic;

namespace GameApi.Quartz4Net
{
    public class ScheduleTaskParameter
    {
        public int Type { get; set; }
        public string ScheduleTaskType { get; set; }
        public int DelaySeconds { get; set; }
        public int RepeatCount { get; set; }
        public string Data { get; set; }
        public string Cron { get; set; } = string.Empty;
        public string JobName { get; set; } = string.Empty;
        public string JobGroup { get; set; } = string.Empty;
        public string Description { get; set; } = string.Empty;
        public string TriggerName { get; set; } = string.Empty;
        public string TriggerGroup { get; set; } = string.Empty;
    }

    public class QuartzOptions
    {
        public bool Enable { get; set; }
        public string JobStoreType { get; set; }
        public string JobStoreTablePrefix { get; set; }
        public string DriverDelegateType { get; set; }
        public string JobStoreDataSource { get; set; }
        public string DataSourceProvider { get; set; }
        public string ConnectionString { get; set; }
        public List<ScheduleTaskParameter> Tasks { get; set; }
    }
}

  

我们新建一个类 ScheduleTaskHostedService 继承IHostedService 接口 ,实现在程序启动的时候,去初始化任务

代码

using GameApi.Quartz4Net.Internals;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Quartz;
using Quartz.Impl;
using System;
using System.Collections.Specialized;
using System.Threading;
using System.Threading.Tasks;

namespace GameApi.Quartz4Net
{
    public class ScheduleTaskHostedService : IHostedService
    {
        // 获取注入的参数
        private readonly QuartzOptions _options;
        // 获取注入的serviceProvider
        private readonly IServiceProvider _serviceProvider;
        // 构造函数
        public ScheduleTaskHostedService(IOptions<QuartzOptions> options,IServiceProvider serviceProvider)
        {
            this._options = options.Value;
            this._serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
        }
        // 实现 IHostedService 接口, 在系统启动时会执行
        public async Task StartAsync(CancellationToken cancellationToken)
        {
            // 验证apollo内的enable参数是否开启
            if (!this._options.Enable) return;
            // 初始化一些属性,用来创建任务对象(单例模式)
            var properties = new NameValueCollection
            {
                // 去Apollo看配置 
                ["quartz.jobStore.type"] = this._options.JobStoreType,
                // 去Apollo看配置 
                ["quartz.jobStore.tablePrefix"] = this._options.JobStoreTablePrefix,
                // 去Apollo看配置 
                ["quartz.jobStore.driverDelegateType"] = this._options.DriverDelegateType,
                // 固定的myDS 
                ["quartz.jobStore.dataSource"] = this._options.JobStoreDataSource,
                // 数据库连接字符串
                ["quartz.dataSource.myDS.connectionString"] = this._options.ConnectionString,
                // 数据库类型
                ["quartz.dataSource.myDS.provider"] = this._options.DataSourceProvider,
                // 序列化方式 有json 和 binary 如果是json 的话,必须nuget引用Quartz.Serialization.Json
                ["quartz.serializer.type"] = "json",
                // 是否多点部署 
                ["quartz.jobStore.clustered"] = "true",
                ["quartz.scheduler.instanceId"] = "AUTO"
            };
            // 初始化一个schedule对象 因为是单例模式的,所以要用一个全局的静态类来存一下
            JobApplicationContext.Scheduler = await new StdSchedulerFactory(properties).GetScheduler();
            // 设置1s的延时启动 没啥特别的,就是记录一下它可以延时启动
            await JobApplicationContext.Scheduler.StartDelayed(TimeSpan.FromSeconds(1d), cancellationToken);

            // 通过生命周期获取 scheduleTaskProvider
            using (var scope = this._serviceProvider.CreateScope())
            {
                //var scheduleTaskProvider = scope.ServiceProvider.GetRequiredService<IScheduleTaskProvider>();

                // 循环获取apollo中的任务列表
                foreach (var parameter in this._options.Tasks)
                {
                    // 获取到任务的Key
                    var jobKey = JobKey.Create(parameter.JobName, parameter.JobGroup);
                    // 验证任务是否已经创建
                    var isExist = await JobApplicationContext.Scheduler.CheckExists(jobKey, cancellationToken);
                    if (isExist) continue;
                    // 没有创建的进行创建
                    var jobName = parameter.JobName;
                    var jobGroup = parameter.JobGroup;
                    // 创建任务明细
                    var jobBuilder = JobBuilder
                        // 这个InnerJob类 是最终执行业务代码的入口类,代码下面给出
                        .Create<InnerJob>()
                        .WithIdentity(jobName, jobGroup)
                        .UsingJobData("type", parameter.Type)
                        .UsingJobData("schedule_task_type", parameter.ScheduleTaskType)
                        .UsingJobData("data", parameter.Data)
                        .UsingJobData("delay_seconds", parameter.DelaySeconds)
                        .UsingJobData("cron", parameter.Cron)
                        .UsingJobData("repeatCount", parameter.RepeatCount)
                        .UsingJobData("repeat_count", parameter.RepeatCount);
                    jobBuilder.WithDescription(parameter.Description ?? string.Empty);
                    var jobDetail = jobBuilder.Build();

                    // 创建触发器
                    // cron 表达式 https://cron.qqe2.com/
                    var triggerBuilder = TriggerBuilder.Create().WithIdentity(jobName, jobGroup);
                    switch (parameter.Type)
                    {
                        case 3: // 基于 cron 表达式的周期性任务
                            if (string.IsNullOrWhiteSpace(parameter.Cron))
                            {
                                throw new ArgumentException("计划任务 cron 表达式不能为空");
                            }

                            triggerBuilder = triggerBuilder.WithCronSchedule(parameter.Cron, builder =>
                            {
                                builder.InTimeZone(TimeZoneInfo.Local);
                            });
                            break;
                        default:
                            throw new ArgumentException("未知任务类型 Type ");
                    }
                    var trigger = triggerBuilder.ForJob(jobDetail.Key).Build();

                    // 创建任务
                    await JobApplicationContext.Scheduler.ScheduleJob(jobDetail, trigger);
                     
                }
            }
        }

        public async Task StopAsync(CancellationToken cancellationToken)
        {
            if (!this._options.Enable) return;

            await JobApplicationContext.Scheduler.Shutdown(cancellationToken);
        }
    }
}

 

定义一个接口 用来反射实现执行任务,凡是继承了这个接口的,并且接口名称与配置文件相匹配的,就执行

using System.Threading.Tasks;

namespace GameApi.Quartz4Net
{
    public interface IScheduleTask
    {
        // 执行计划任务。
        Task RunAsync(ScheduleTaskContext context);
    }
}

  

下面是InnerJob类,他继承了IJob,通俗的来说,就是触发器的实现,下面用反射的思路来实现

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Quartz;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace GameApi.Quartz4Net.Internals
{
    internal class InnerJob : IJob
    {
        public InnerJob()
        {

        }
        public async Task Execute(IJobExecutionContext context)
        {
            using (var childScope = JobApplicationContext.IoC.CreateScope())
            {
                var logger = childScope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger<InnerJob>();
                var scheduleTaskType = context.JobDetail.JobDataMap.GetString("schedule_task_type");
                var data = context.JobDetail.JobDataMap.GetString("data");
                var delaySeconds = context.JobDetail.JobDataMap.GetInt("delay_seconds");

                foreach (var kv in context.JobDetail.JobDataMap)
                {
                    logger.LogInformation("计划任务参数:{0} = {1}", kv.Key, kv.Value);
                }

                var type = AssemblyHelper.GetType(scheduleTaskType);
                if (type == null)
                {
                    logger.LogWarning("未找到指定的任务类型: {0}", scheduleTaskType);
                    return;
                }
                var instances = childScope.ServiceProvider.GetServices(type);
                if (instances == null || !instances.Any()) return;

                foreach (var obj in instances)
                {
                    var instance = obj as IScheduleTask;
                    if (instance == null)
                    {
                        logger.LogWarning("任务 {0} 必须继承 {1} 接口", scheduleTaskType, nameof(IScheduleTask));
                        continue;
                    }
                    try
                    {
                        var scheduleTaskContext = new ScheduleTaskContext
                        {
                            Data = data,
                            TaskId = context.JobDetail.Key.Name,
                            TaskGroup = context.JobDetail.Key.Group
                        };
                        await instance.RunAsync(scheduleTaskContext);
                    }
                    catch (Exception e)
                    {
                        logger.LogError(e, "调用计划任务异常。");
                    }
                }
            }
        }
    }

    public static class AssemblyHelper
    {
        private static List<Assembly> _applicationAssemblies { get; } =
            AppDomain.CurrentDomain.GetAssemblies()
            .Where(asm => asm.FullName.StartsWith("GameApi.", StringComparison.OrdinalIgnoreCase)).ToList();

        /// <summary>
        /// 通过指定的类型名称获取一个类型 <see cref="Type"/> 。
        /// </summary>
        /// <param name="typeFullName"></param>
        /// <returns></returns>
        public static Type GetType(string typeFullName)
        {
            var items = typeFullName.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries);
            var builder = new StringBuilder(typeFullName.Length);
            var index = items.Length;
            Assembly assembly = null;
            while (index > 0)
            {
                builder.Clear();
                if (index < 0) return null;
                for (var i = 0; i < index; i++)
                {
                    builder.Append(items[i]).Append('.');
                }
                builder.Remove(builder.Length - 1, 1);
                --index;
                assembly = _applicationAssemblies.FirstOrDefault(_ => _.FullName.StartsWith(builder.ToString(), StringComparison.OrdinalIgnoreCase));
                if (assembly != null)
                {
                    break;
                }
            }
            var type = assembly.GetType(typeFullName, false, true);
            return type;
        }
    }
}

 

然后我们把  ScheduleTaskHostedService  这个类,注入到容器内,为了可扩展性,这种东西我们一般都写一个扩展类来实现,我们在新建一个 ServiceCollectionExtensions 类

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace GameApi.Quartz4Net
{
    public static class ServiceCollectionExtensions
    {
        // 扩展类 添加 Quartz.NET 组件支持。
        public static IServiceCollection AddQuartz(this IServiceCollection services, IConfiguration configuration)
        {
            // 单例注入
            services.AddSingleton<IHostedService, ScheduleTaskHostedService>();
            // 配置
            services.Configure<QuartzOptions>(configuration.GetSection("Quartz"));
            return services;
        }
    }
}

 

然后在startup里,add一下

 services.AddQuartz(this.Configuration);

 

 

至此,服务端的实现已经完毕,接下来就是怎么用了

 

我们回头来解读一下apollo的配置文件,只说重要的部分

DataSourceProvider 和 ConnectionString 是数据库相关的配置

Tasks 这个就是一个job集合,可以是多个,下面是task子节点的说明

JobName 名称,需要全局唯一,不可为空

JobGroup 分组,不能为空

ScheduleTaskType 任务类型,这个下面在定义客户端的时候会用到,因为是用反射执行代码,所以这个一定不可以错,错i的话任务创建成功了但是不会执行

Type 任务执行的方式 目前只实现了基于Cron表达式的方式,其他的可以自己去查文档

Cron 周期表达式 参考  https://cron.qqe2.com/

Data 自定义参数

Description 任务描述(可以用来看日志)

 

4.4 执行任务

上面已经把任务定义好了,那么怎么实现呢,在4..3结尾的地方说到了用反射的方式去执行任务,下面给出具体实现,其实就是实现上面定义的IScheduleTask接口

先定义一个接口,继承 IScheduleTask 接口(面向对象已经过时了,要面向接口)

using GameApi.Quartz4Net;

namespace GameApi.Web.ScheduleTask
{
    public interface ITest : IScheduleTask
    {
    }
}

注意,上面Apollo里面的ScheduleTaskType参数,是

GameApi.Web.ScheduleTask.ITest

这个接口的名字和namespace,要跟上面的配置文件对应上

 

然后实现这个test接口

using GameApi.Quartz4Net;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

namespace GameApi.Web.ScheduleTask
{
    public class Test : ITest
    {
        private readonly ILogger<Test> _logger;
        public Test(ILogger<Test> logger)
        {
            this._logger = logger;
        }
        public Task RunAsync(ScheduleTaskContext context)
        {
            this._logger.LogInformation($"当前时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}");

            return Task.CompletedTask;
        }
    }
}

 

同样,注入到service里面

services.AddTransient<ITest, Test>();

 

F5 运行 ,结果如下

 

接下来多点部署,看会不会起冲突,其实就是试试

 ["quartz.jobStore.clustered"] = "true",

这个参数好不好使

起了8801,8802两个端口,结果如下

 

 结果证明,多点部署后,只会打到一个节点上,不会有多点消费的情况

 

posted @ 2020-05-29 19:05  AresYM  阅读(563)  评论(0编辑  收藏  举报