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两个端口,结果如下

结果证明,多点部署后,只会打到一个节点上,不会有多点消费的情况
浙公网安备 33010602011771号