专栏首页Dotnet Plus3分钟掌握Quartz.net分布式定时任务的姿势

3分钟掌握Quartz.net分布式定时任务的姿势

引言

长话短说,今天聊一聊分布式定时任务,我的流水账笔记:

细心朋友稍一分析,就知道还有问题: 水平扩展后的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库
  • https://www.quartz-scheduler.net/documentation/quartz-2.x/tutorial/job-stores.html
  • https://github.com/guryanovev/CrystalQuartz

本文分享自微信公众号 - Dotnet Plus(nodotnet),作者:小码甲

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-04-18

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • ASP.NET Core+Quartz.Net实现web定时任务

    作为一枚后端程序狗,项目实践常遇到定时任务的工作,最容易想到的的思路就是利用Windows计划任务/wndows service程序/Crontab程序等主机方...

    小码甲
  • 解读三组容易混淆的Dockerfile指令

    COPY、ADD主体功能类似:从指定位置src拷贝文件到Docker镜像dest。

    小码甲
  • 前后端分离,如何在前端项目中动态插入后端API基地址?(in docker)

    开门见山,本文分享前后端分离,容器化前端项目时动态插入后端API基地址,这是一个很赞的实践,解决了前端项目容器化过程中受制后端调用的尴尬。

    小码甲
  • WCF系列教程之WCF服务配置工具

    本文参考自http://www.cnblogs.com/wangweimutou/p/4367905.html Visual studio 针对服务配置提供了一...

    郑小超.
  • DOM盒子模型常用属性client,offset和scroll

    [获取元素具体的某个样式值] 1.[元素].style.xxx 操作获取 只能获取所有写在元素行内上的样式(不写在行内上,不管你写没写都获取不到,真实项目...

    TimothyJia
  • 常见问题:MongoDB诊断

    ·为什么MongoDB会记录这么多“Connection Accepted”事件?

    MongoDB中文社区
  • seek()对中文偏移测试

    skylark
  • [软件开发]·Windows系统安装MySQL简易教程

    下载地址:https://dev.mysql.com/downloads/mysql/

    小宋是呢
  • SpringBoot2.0 基础案例(06):引入JdbcTemplate,和多数据源配置

    在Spring Boot2.0框架下配置数据源和通过JdbcTemplate访问数据库的案例。 SpringBoot对数据库的操作在jdbc上面做了深层次的封装...

    知了一笑
  • [C语言] 数据结构-算法效率的度量方法-事前分析估算方法

    事前分析估算方法:在计算机程序编制前,依据统计方法对算法进行估算,抛开与计算机硬件软件有关的因素,一个程序的运行时间,依赖于算法的,好坏和问题的输入规模,所谓问...

    陶士涵

扫码关注云+社区

领取腾讯云代金券