introduction
To make a long story short, talk about distributed timing tasks today. My journal notes:
- ASP.NET Core + Quartz.Net implements web timing tasks
- AspNetCore combined with Redis practice message queue
After careful analysis, friends know that there is still a problem: the
horizontally expanded WebApp Quartz.net scheduled task will be triggered multiple times,
because the webapp instance uses the default, RAMJobStore
and multiple instances maintain copies of Job and Trigger in memory. .
My scheduled task is a synchronous task. It is not a big problem to execute it multiple times, but for a scheduled task of a specific business, multiple executions may be a fatal problem.
Based on this, take a look at the pose of Quartz.net distributed timing tasks
AdoJobStore
Obviously, horizontally expanding multi-instances require a mechanism independent of web instances to store Job and Trigger.
Quartz.NET provides ADO.NET JobStore to store job data.
- First use SQL script to generate the specified table structure in the database
After executing the script, you will see a few more tables starting with QRTZ_ in the database
- Configure Quartz.net to use AdoJobStore
The configuration can be added in the form of encoding or quartz.config
Quick practice
1. Generate Job and Trigger tables in advance
Download the appropriate database table script from https://github.com/quartznet/quartznet/tree/master/database/tables to generate the specified table structure
2. Add AdoJobStore
This time, the AdoJobStore configuration is added using encoding.
The first start will persist the Job and Trigger in the code to sqlite, and then directly load the Job and Trigger from sqlite
using System;
using System.Collections.Specialized;
using System.Data;
using System.Threading.Tasks;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging;
using Quartz;
using Quartz.Impl;
using Quartz.Impl.AdoJobStore.Common;
using Quartz.Spi;
namespace EqidManager
{
using IOCContainer = IServiceProvider;
public class QuartzStartup
{
public IScheduler Scheduler { get; set; }
private readonly ILogger _logger;
private readonly IJobFactory iocJobfactory;
public QuartzStartup(IOCContainer IocContainer, ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<QuartzStartup>();
iocJobfactory = new IOCJobFactory(IocContainer);
DbProvider.RegisterDbMetadata("sqlite-custom", new DbMetadata()
{
AssemblyName = typeof(SqliteConnection).Assembly.GetName().Name,
ConnectionType = typeof(SqliteConnection),
CommandType = typeof(SqliteCommand),
ParameterType = typeof(SqliteParameter),
ParameterDbType = typeof(DbType),
ParameterDbTypePropertyName = "DbType",
ParameterNamePrefix = "@",
ExceptionType = typeof(SqliteException),
BindByName = true
});
var properties = new NameValueCollection
{
["quartz.jobStore.type"] = "Quartz.Impl.AdoJobStore.JobStoreTX, Quartz",
["quartz.jobStore.useProperties"] = "true",
["quartz.jobStore.dataSource"] = "default",
["quartz.jobStore.tablePrefix"] = "QRTZ_",
["quartz.jobStore.driverDelegateType"] = "Quartz.Impl.AdoJobStore.SQLiteDelegate, Quartz",
["quartz.dataSource.default.provider"] = "sqlite-custom",
["quartz.dataSource.default.connectionString"] = "Data Source=EqidManager.db",
["quartz.jobStore.lockHandler.type"] = "Quartz.Impl.AdoJobStore.UpdateLockRowSemaphore, Quartz",
["quartz.serializer.type"] = "binary"
};
var schedulerFactory = new StdSchedulerFactory(properties);
Scheduler = schedulerFactory.GetScheduler().Result;
Scheduler.JobFactory = iocJobfactory;
}
public async Task<IScheduler> ScheduleJob()
{
var _eqidCounterResetJob = JobBuilder.Create<EqidCounterResetJob>()
.WithIdentity("EqidCounterResetJob")
.Build();
var _eqidCounterResetJobTrigger = TriggerBuilder.Create()
.WithIdentity("EqidCounterResetCron")
.StartNow()
//每天凌晨0s
.WithCronSchedule("0 0 0 * * ?") Seconds,Minutes,Hours,Day-of-Month,Month,Day-of-Week,Year(optional field)
.Build();
// 这里一定要先判断是否已经从SQlite中加载了Job和Trigger
if (!await Scheduler.CheckExists(new JobKey("EqidCounterResetJob")) &&
!await Scheduler.CheckExists(new TriggerKey("EqidCounterResetCron")))
{
await Scheduler.ScheduleJob(_eqidCounterResetJob, _eqidCounterResetJobTrigger);
}
await Scheduler.Start();
return Scheduler;
}
public void EndScheduler()
{
if (Scheduler == null)
{
return;
}
if (Scheduler.Shutdown(waitForJobsToComplete: true).Wait(30000))
Scheduler = null;
else
{
}
_logger.LogError("Schedule job upload as application stopped");
}
}
}
The above is the core code of Quartz.NET loading Job and Trigger from sqlite
Here are two tips:
①. IOCJobFactory is a custom JobFactory, the purpose is to combine with ASP.NET Core native dependency injection
②. When scheduling tasks, we must first determine whether Job and Trigger have been loaded from sqlite
3. Add Quartz.Net UI wheel
Comes with Quartz.NET's scheduling UI: CrystalQuartz, to facilitate scheduling tasks on the interface
① Install-Package CrystalQuartz.AspNetCore -IncludePrerelease
② Startup enables CrystalQuartz
using CrystalQuartz.AspNetCore;
/*
* app is IAppBuilder
* scheduler is your IScheduler (local or remote)
*/
var quartz = app.ApplicationServices.GetRequiredService<QuartzStartup>();
var _schedule = await quartz.ScheduleJob();
app.UseCrystalQuartz(() => scheduler);
③ Check the schedule at localhost: YOUR_PORT / quartz address
Summary output
- Quartz.net uses AdoJobStore to support distributed timing tasks, solving the problem of multiple instances triggered multiple times
- Quickly throw the wheel: Quartz.Net UI library