3分鐘掌握Quartz.net分佈式定時任務的姿勢

引言

長話短說,今天聊一聊分佈式定時任務,我的流水賬筆記:

  • ASP.NET Core+Quartz.Net實現web定時任務
  • AspNetCore結合Redis實踐消息隊列

細心朋友稍一分析,就知道還有問題:
水平擴展後的WebApp的Quartz.net定時任務會多次觸發,
因爲webapp實例使用的是默認的RAMJobStore, 多實例在內存中都維護了Job和Trigger的副本.

我的定時任務是同步任務,多次執行倒是沒有太大問題,但對於特定業務的定時任務, 多次執行可能是致命問題。

基於此,來看看Quartz.net 分佈式定時任務的姿勢

AdoJobStore

很明顯,水平擴展的多實例需要一個 獨立於web實例的機制來存儲Job和Trigger.

Quartz.NET提供ADO.NET JobStore來存儲任務數據。

  1. 先使用SQL腳本在數據庫中生成指定的表結構

執行腳本之後,會看到數據庫中多出幾個以 QRTZ_開頭的表

  1. 配置Quartz.net使用AdoJobStore

可採用編碼形式或者 quartz.config形式添加配置

快速實踐

1. 預先生成Job、Trigger表

從https://github.com/quartznet/quartznet/tree/master/database/tables 下載合適的數據庫表腳本, 生成指定的表結構

2. 添加AdoJobStore

本次使用編碼方式添加AdoJobStore配置。
首次啓動會將代碼中Job和Trigger持久化到sqlite,後面就直接從sqlite中加載Job和Trigger

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");
        }
    }
}

上面是Quartz.NET 從sqlite中加載Job和Trigger的核心代碼

這裏要提示兩點:

①. IOCJobFactory 是自定義JobFactory,目的是與ASP.NET Core原生依賴注入結合
②. 在調度任務的時候, 要先判斷是否已經從sqlite加載了Job和Trigger

3.添加Quartz.Net UI輪子

附贈Quartz.NET的調度UI: CrystalQuartz,方便在界面管理調度任務
① Install-Package CrystalQuartz.AspNetCore -IncludePrerelease
② Startup啓用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);

③ 在localhost:YOUR_PORT/quartz地址查看調度

總結輸出

  1. Quartz.net以AdoJobStore支撐分佈式定時任務,解決多實例多次觸發的問題
  2. 快速拋出輪子:Quartz.Net UI庫
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章