Scheduler-Quartz.Net 使用原理及教程
Scheduler-Quartz.Net 使用原理及教程
1.简介
这一篇会相对比较跳跃和简洁,一方面是官方文档很详尽,另外就是Scheduler最重要的是理解其中的思想!!!
Quartz.NET是一个开源的job调度框架,是OpenSymphony 的 Quartz API的.NET移植,它用C#写成,可用于winform和asp.net应用中。它具有良好的灵活性而且配置操作简单。你能够用它执行一个Job来实现简单或复杂的调度。它有很多特征,如:数据库支持,集群,插件,支持cron-like表达式等等。
它有如下几个特点:
-
API 操作简单,只要几行简单的代码你就可以在应用程序里面实现自己的Job调度,并实时监视Job的执行情况
-
触发器功能强大,比 Windows 的任务计划提供更细的触发粒度,你可以使用“Cron表达式(后文将介绍)”来实现如:每周星期一到星期五 8:00am,5:00pm(工作时间) 执行某一件任务
-
良好的可扩展性,它基于接口编程,你可以实现自己的 Schedule 调度器,Job 作业,以及 Trigger 触发器等
-
Job可以保存在 RAM 中,也可以持久化到数据库,支持多种数据库类型:SqlServer、Oracle、MySql等
-
集群,这是一个高级应用,可以在多台计算机之间创建负载平衡、容错处理
详情请查看:Quartz.net官网
官网说的其实很明白,这里就简单写一下,文章的最后会提供分布式的Quartz Code Demo,市面上实现Schedule的工具有很多,quartz 只是其中一个,主要的还是要理解其中的思想。
Scheduler的需求是什么,简单来说就是:根据调用方给定的参数和时间来执行某种操作!
一个最简单的思路是,在DB 中维护一张表,里面存储了需要执行的jobdetail,time,status,通过一个windows service来进行检索和调用 ,当达到执行时间的时候,从DB中检索出来需要执行的Job,在执行完成后,更新对应的状态。
当然 这其中需要注意的事项有很多很多,这只是实现job 的思路而已,在工作中可以多想想!
2..Net Core DI 集成
1.需要安装quartz的NuGet的扩展包
Install-Package Quartz.Extensions.DependencyInjection
2.Startup.ConfigureServices
这个是官网提供的code,而我们在实际开发过程中,根本使用不到这么多配置,但不妨碍了解一下吧。
public void ConfigureServices(IServiceCollection services)
{
// base configuration from appsettings.json
services.Configure<QuartzOptions>(Configuration.GetSection("Quartz"));
// if you are using persistent job store, you might want to alter some options
services.Configure<QuartzOptions>(options =>
{
options.Scheduling.IgnoreDuplicates = true; // default: false
options.Scheduling.OverWriteExistingData = true; // default: true
});
services.AddQuartz(q =>
{
// handy when part of cluster or you want to otherwise identify multiple schedulers
q.SchedulerId = "Scheduler-Core";
// we take this from appsettings.json, just show it's possible
// q.SchedulerName = "Quartz ASP.NET Core Sample Scheduler";
// as of 3.3.2 this also injects scoped services (like EF DbContext) without problems
q.UseMicrosoftDependencyInjectionJobFactory();
// or for scoped service support like EF Core DbContext
// q.UseMicrosoftDependencyInjectionScopedJobFactory();
// these are the defaults
q.UseSimpleTypeLoader();
q.UseInMemoryStore();
q.UseDefaultThreadPool(tp =>
{
tp.MaxConcurrency = 10;
});
// quickest way to create a job with single trigger is to use ScheduleJob
// (requires version 3.2)
q.ScheduleJob<ExampleJob>(trigger => trigger
.WithIdentity("Combined Configuration Trigger")
.StartAt(DateBuilder.EvenSecondDate(DateTimeOffset.UtcNow.AddSeconds(7)))
.WithDailyTimeIntervalSchedule(x => x.WithInterval(10, IntervalUnit.Second))
.WithDescription("my awesome trigger configured for a job with single call")
);
// you can also configure individual jobs and triggers with code
// this allows you to associated multiple triggers with same job
// (if you want to have different job data map per trigger for example)
q.AddJob<ExampleJob>(j => j
.StoreDurably() // we need to store durably if no trigger is associated
.WithDescription("my awesome job")
);
// here's a known job for triggers
var jobKey = new JobKey("awesome job", "awesome group");
q.AddJob<ExampleJob>(jobKey, j => j
.WithDescription("my awesome job")
);
q.AddTrigger(t => t
.WithIdentity("Simple Trigger")
.ForJob(jobKey)
.StartNow()
.WithSimpleSchedule(x => x.WithInterval(TimeSpan.FromSeconds(10)).RepeatForever())
.WithDescription("my awesome simple trigger")
);
q.AddTrigger(t => t
.WithIdentity("Cron Trigger")
.ForJob(jobKey)
.StartAt(DateBuilder.EvenSecondDate(DateTimeOffset.UtcNow.AddSeconds(3)))
.WithCronSchedule("0/3 * * * * ?")
.WithDescription("my awesome cron trigger")
);
// you can add calendars too (requires version 3.2)
const string calendarName = "myHolidayCalendar";
q.AddCalendar<HolidayCalendar>(
name: calendarName,
replace: true,
updateTriggers: true,
x => x.AddExcludedDate(new DateTime(2020, 5, 15))
);
q.AddTrigger(t => t
.WithIdentity("Daily Trigger")
.ForJob(jobKey)
.StartAt(DateBuilder.EvenSecondDate(DateTimeOffset.UtcNow.AddSeconds(5)))
.WithDailyTimeIntervalSchedule(x => x.WithInterval(10, IntervalUnit.Second))
.WithDescription("my awesome daily time interval trigger")
.ModifiedByCalendar(calendarName)
);
// also add XML configuration and poll it for changes
q.UseXmlSchedulingConfiguration(x =>
{
x.Files = new[] { "~/quartz_jobs.config" };
x.ScanInterval = TimeSpan.FromSeconds(2);
x.FailOnFileNotFound = true;
x.FailOnSchedulingError = true;
});
// convert time zones using converter that can handle Windows/Linux differences
q.UseTimeZoneConverter();
// auto-interrupt long-running job
q.UseJobAutoInterrupt(options =>
{
// this is the default
options.DefaultMaxRunTime = TimeSpan.FromMinutes(5);
});
q.ScheduleJob<SlowJob>(
triggerConfigurator => triggerConfigurator
.WithIdentity("slowJobTrigger")
.StartNow()
.WithSimpleSchedule(x => x.WithIntervalInSeconds(5).RepeatForever()),
jobConfigurator => jobConfigurator
.WithIdentity("slowJob")
.UsingJobData(JobInterruptMonitorPlugin.JobDataMapKeyAutoInterruptable, true)
// allow only five seconds for this job, overriding default configuration
.UsingJobData(JobInterruptMonitorPlugin.JobDataMapKeyMaxRunTime, TimeSpan.FromSeconds(5).TotalMilliseconds.ToString(CultureInfo.InvariantCulture)));
// add some listeners
q.AddSchedulerListener<SampleSchedulerListener>();
q.AddJobListener<SampleJobListener>(GroupMatcher<JobKey>.GroupEquals(jobKey.Group));
q.AddTriggerListener<SampleTriggerListener>();
// example of persistent job store using JSON serializer as an example
/*
q.UsePersistentStore(s =>
{
s.UseProperties = true;
s.RetryInterval = TimeSpan.FromSeconds(15);
s.UseSqlServer(sqlServer =>
{
sqlServer.ConnectionString = "some connection string";
// this is the default
sqlServer.TablePrefix = "QRTZ_";
});
s.UseJsonSerializer();
s.UseClustering(c =>
{
c.CheckinMisfireThreshold = TimeSpan.FromSeconds(20);
c.CheckinInterval = TimeSpan.FromSeconds(10);
});
});
*/
});
// we can use options pattern to support hooking your own configuration
// because we don't use service registration api,
// we need to manually ensure the job is present in DI
services.AddTransient<ExampleJob>();
services.Configure<SampleOptions>(Configuration.GetSection("Sample"));
services.AddOptions<QuartzOptions>()
.Configure<IOptions<SampleOptions>>((options, dep) =>
{
if (!string.IsNullOrWhiteSpace(dep.Value.CronSchedule))
{
var jobKey = new JobKey("options-custom-job", "custom");
options.AddJob<ExampleJob>(j => j.WithIdentity(jobKey));
options.AddTrigger(trigger => trigger
.WithIdentity("options-custom-trigger", "custom")
.ForJob(jobKey)
.WithCronSchedule(dep.Value.CronSchedule));
}
});
// Quartz.Extensions.Hosting allows you to fire background service that handles scheduler lifecycle
services.AddQuartzHostedService(options =>
{
// when shutting down we want jobs to complete gracefully
options.WaitForJobsToComplete = true;
});
}
这事我实际应用时添加配置项。
services.AddSingleton<IJobFactory, QuartzJobFactory>();
services.AddSingleton<ISchedulerFactory, StdSchedulerFactory>(provider =>
{
var options = new NameValueCollection();
options.Set("quartz.jobStore.clustered", 集群属性);
options.Set("quartz.threadPool.maxConcurrency", 最多可以同时运行Job数);
options.Set("quartz.scheduler.instanceName", 调度程序的name);
options.Set("quartz.scheduler.instanceId", 调度程序的ID);
options.Set("quartz.scheduler.batchTriggerAcquisitionMaxCount", 最多可以同时触发的job数);
options.Set("quartz.jobStore.acquireTriggersWithinLock", 锁定状态下的触发器);
options.Set("quartz.serializer.type", "json");
options.Set("quartz.jobStore.type", "Quartz.Impl.AdoJobStore.JobStoreTX, Quartz");
options.Set("quartz.jobStore.useProperties", "true");
options.Set("quartz.jobStore.driverDelegateType", Quartz.Impl.AdoJobStore.SqlServerDelegate, Quartz);
options.Set("quartz.jobStore.tablePrefix", "qrtz_");
options.Set("quartz.jobStore.dataSource", "myDS");
options.Set("quartz.dataSource.myDS.provider", "SqlServer");
options.Set("quartz.dataSource.myDS.connectionString", db.ConnectionString);
return new StdSchedulerFactory(options);
});
services.AddSingleton(provider =>
{
var schedulerFactory = provider.GetService<ISchedulerFactory>();
var scheduler = schedulerFactory.GetScheduler().Result;
scheduler.JobFactory = provider.GetService<IJobFactory>();
FailedJobListener failedJobListener = new FailedJobListener(//RetryIntervalSeconds, //RetryCount);
scheduler.ListenerManager.AddJobListener(failedJobListener);
SchedulerListener schedulerListener = new SchedulerListener();
scheduler.ListenerManager.AddSchedulerListener(schedulerListener);
//scheduler.Start();
return scheduler;
3.Create trigger
//执行一次
return TriggerBuilder .Create() .WithIdentity(triggerId, schedule.JobType)//认证信息 .StartAt(startTime)//StartTime->UTC .WithSimpleSchedule(x => x.WithMisfireHandlingInstructionFireNow()) .Build();
//Cron 表达式,一个task
return TriggerBuilder .Create() .WithIdentity(triggerId, schedule.JobType) .WithCronSchedule(schedule.RunTime)// Cron .Build();
4.Create job Detail
return JobBuilder
.Create(jobType)
.RequestRecovery(true)
.UsingJobData(JobKey, key)
.WithIdentity(schedule.JobName, schedule.JobType)
.WithDescription(schedule.JobName)
.Build();
5.将Trigger和JobDetail关联起来
//
// Summary:
// Add the given Quartz.IJobDetail to the Scheduler, and associate the given Quartz.ITrigger
// with it.
//
// Remarks:
// If the given Trigger does not reference any Quartz.IJob, then it will be set
// to reference the Job passed with it into this method.
Task<DateTimeOffset> ScheduleJob(IJobDetail jobDetail, ITrigger trigger, CancellationToken cancellationToken = default(CancellationToken));
6.执行job
[P [PersistJobDataAfterExecution]
[DisallowConcurrentExecution]
public class RunJobService : IJob
{
public async Task Execute(IJobExecutionContext context)
{
string jobName = string.Empty;
try
{
//check
string content = dataMap.GetString(QuartzConstants.JobContentKey);
string type = dataMap.GetString(QuartzConstants.JobTypeKey);
if (!string.IsNullOrEmpty(type))
{
await 执行job的方法;
}
else
{
throw new Exception("JobType is not exist.");
}
}
catch (JobExecutionException ex)
{
//logger
throw ex;
}
}
}
浙公网安备 33010602011771号