Quartz.Net分布式任务管理平台

   前言:我相信大多数人公司的业务上都有定时任务这么个功能,我们公司也不例外,刚来公司的时候使用Quartz.Net为我们组做了第一个任务,大致流程是:新建一个控制台程序,引用需要的程序集,Execute方法中写着咱们需要定时的任务的业务逻辑,同样这边需要用的一些数据库操作类引用的是WebApi项目中的一些类库,然后拿着服务去服务器上部署。随着时间的推移问题和不方便性慢慢被暴露,总结一下这样的方式在我公司发生的问题

第一:随着任务量的增加,我会分配新的同事去开发这样的任务,可能由于我没有交流清楚,导致新的同事会自己下载所需的程序集,上面已经说了服务会引用WebApi中的类库会导致同样的程序集本版不匹配,同样改变Api中的一些程序集依然会出现这样的问题。

第二:业务逻辑集成在了服务类中很多时候产品“大大”会更改逻辑由于部署的方式 那么我们需要下载服务重新编译再次部署。

第三:任务在服务器上没有监听那么意味着服务挂了我们没有感知,案例:我们组会和其他组有着共同的任务,由于一些原因导致数据可能没有正确的相应,这个服务会去检测,客户问我昨晚的数据怎么到现在还是这样,排查后发现服务已经挂了。

第四:我们的服务中充满了业务逻辑导致逻辑分散,同样压力交给了定时服务,那么会导致服务的运行周期和任务执行的时间会冲突。

第五:就是可能部署的人今天请假了,导致这样的部署方式变得不在那么容易。

那么问题出现了,看见了总不能当作没发生,开始自行研究得到今天要分享的主题,但是针对上面的问题我相信很多的解决方案,在各位园友的公司中使用了上述方式没问题也是有可能的。


      主题:为了解决上面的问题,便捷性,做出如下“架构图”:

 

对应我们的项目层次图如下:

说一下两幅图的对应关系按顺序称一图和二图:

一图中的Quartz服务节点对应这二图中的Quartz.Net_RemoteServer

一图中的任务基类对应着二图中的Quartz.Net_JobBase

一图中的任务子类对应着二图中的TestJob1,TestJob2

对于Mvc站点中我们使用了两层,关于分层这一说比较艺术不作过多讨论,我们项目中没有什么业务逻辑Web直接和“仓储”直接做交互,使用的是EF(ORM);表现层我们使用BootStrap和Vue.Js完成前端工作。· 

先简单对一图作一下解释:

第一部分:Quartz服务节点是我们任务运行的调度器,既然是分布式,我们会将调度器部署在三台服务器,Quartz默认是基于内存的既然我们要分布式 ,我们需要持久化,本版本是基于SqlServer,同时Quartz框架在数据库中用锁实现了每个任务只会被一台服务器调用,那么当某个服务器上的调度器挂掉之后,Quartz会检测发现挂了之后会使用其他服务器上的节点来接手挂掉节点中的所有任务,这个都是Quartz自身提供的。具体我们看Quartz.Net_RemoteServer类库:

 

Program类中我们将配置调度器这里使用代码配置:

class Program

    {

        static void Main(string[] args)


        {

            var properties = new NameValueCollection();

            properties["quartz.scheduler.instanceName"] = "RemoteServer";

            properties["quartz.threadPool.type"] = "Quartz.Simpl.SimpleThreadPool, Quartz";

            properties["quartz.threadPool.threadCount"] = "5";

            properties["quartz.threadPool.threadPriority"] = "Normal";

            properties["quartz.scheduler.exporter.type"] = "Quartz.Simpl.RemotingSchedulerExporter, Quartz";

            properties["quartz.scheduler.exporter.port"] = "555";//端口号

            properties["quartz.scheduler.exporter.bindName"] = "QuartzScheduler";//名称

            properties["quartz.scheduler.exporter.channelType"] = "tcp";//通道名

            properties["quartz.scheduler.exporter.channelName"] = "httpQuartz";

            properties["quartz.scheduler.exporter.rejectRemoteRequests"] = "true";

            properties["quartz.jobStore.clustered"] = "true";//集群配置

            //下面为指定quartz持久化数据库的配置

            properties["quartz.jobStore.type"] = "Quartz.Impl.AdoJobStore.JobStoreTX, Quartz";

            properties["quartz.jobStore.tablePrefix"] = "QRTZ_";

            properties["quartz.jobStore.driverDelegateType"] = "Quartz.Impl.AdoJobStore.SqlServerDelegate, Quartz";

            properties["quartz.jobStore.dataSource"] = "myDS";

            properties["quartz.dataSource.myDS.connectionString"] = @"Data Source=.;Initial Catalog=QuartzManager;User ID=sa;Password=123456";

            properties["quartz.dataSource.myDS.provider"] = "SqlServer-20";

            properties["quartz.scheduler.instanceId"] = "AUTO";

            var schedulerFactory = new StdSchedulerFactory(properties);

            var scheduler = schedulerFactory.GetScheduler();

            scheduler.ListenerManager.AddJobListener(new MyJobListener(), GroupMatcher<JobKey>.AnyGroup());

            scheduler.Start();


        }

    }

这是Quartz代码配置,很多项感兴趣的大家可以自行搜索一下意思;这边注释了客户端将要用的信息,同时我们增加监听器来监听运行服务的状况,Quartz提供了三种监听IJobListener,ITriggerListener,ISchedulerListener,和相应的实现类JobListenerSupport,TriggerListenerSupport,SchedulerListenerSupport,在这里我们使用的job的监听器同时我们继承实现类就好了,因为很多很方法不需要。

我们会在任务完成时向数据库中写入此任务当前状态,或者异常信息

jobId = Convert.ToInt32(context.JobDetail.JobDataMap["jobId"]);

                name = context.Scheduler.GetTriggerState(context.Trigger.Key).ToString();

                triggerState = _changeType(context.Scheduler.GetTriggerState(context.Trigger.Key));

                customerJobInfoModel = _customerJobInfoRepository.LoadCustomerInfo(x => x.Id == jobId);

                customerJobInfoModel.TriggerState = triggerState;

                if (jobException == null)

                {



                    Console.WriteLine("任务编号{0};执行时间:{1},状态:{2}", context.JobDetail.JobDataMap["jobId"], DateTime.Now, name);

                }

                else

                {

                    customerJobInfoModel.Exception = jobException.Message;

                    Console.WriteLine("jobId{0}执行失败:{1}", context.JobDetail.JobDataMap["jobId"], jobException.Message);

                }

                _customerJobInfoRepository.UpdateCustomerJobInfo(customerJobInfoModel);

第二部分:要想实现任务执行我们得实现Quartz提供得Excute方法,也就是咱们的具体任务类,任务基类的设计来目的是:首先它是个抽象类,我们的任务基类会引用所需程序集并实现Excute方法,并提供子类必须要实现的方法,这样我们具体开发任务的时候继承我们的基类实现基类中的方法 而我们任务子类除了引用了基类不会在引用其他第三方程序集。继续看Quartz.Net_RemoteServer类库:

 

在这里就像看到一样我们将需要的程序集在基类中引用包括Quartz.dll,日志等,BaseJob封装了任务执行所必要的方法,和子类必须实现的方法:

public abstract class JobBase : IJob

    {

        public string RequestUrl { get; set; }

        public JobBase()

        {

            SetRequestUrl();

        }

        public virtual void Execute(IJobExecutionContext context)

        {

            try

            {

                HttpClient hc = new HttpClient();

                hc.GetAsync(RequestUrl);

            }

            catch (Exception ex)

            {

                throw new Exception(ex.ToString());

            }

        }

        public abstract void SetRequestUrl();

    }

同时可以看到我们的执行的任务本质上就是调用了Api中实现业务的接口,至此我们将业务逻辑和和部分压力点转移到Api中是我们任务做的东西和职责很明确:根据运行周期来执行一个业务,业务本身并不属于自己的功能点,接下来我们在看看具体子类,上面的这些东西在我这边完成后,想加任务的同事只需要编写任务子类就可以在通过管理界面操作,子类的代码如下:

using Quartz.Net_JobItem;

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Threading.Tasks;


namespace TestJob1

{

    public class Job1:JobBase

    {

        public Job1() {

            SetRequestUrl();

        }

        public override void SetRequestUrl()

        {

            base.RequestUrl = "http://localhost:53582/QuartzJobManage/Test";

        }

    }

}

可以看到已经简单的动动小手指就可以了,轻松完成,同时咱们可以看到我们除了引用了基类没有引用其他第三方的框架,来避免上面的问题出现。基类在编译dll已经放入了Quartz的服务节点和Web站点中。子类我们同样需要放入着两个类库中,方便Quartz服务节点可以找到要执行的任务,和Web站点做反射需要

第三部分:Mvc站点将提供可视化的管理界面。首先看一下界面的模样:

导航栏是Quartz任务所有的状态,列表是我们任务的具体信息,在一图中中我们将在任务可视化界面中提供:添加任务,运行任务,修改任务运行周期,暂停任务,恢复任务,删除任务操作,也是目前我们需要在公司上线的第一个版本所有功能点。每个状态下的任务能使用的功能会有所不一样,接下来将逐一展示以上提到的功能点实现:

1:添加功能:我们需要将Quartz需要的任务的和我们需要的信息存入到我们自己的表中,以便运行任务的时候从表中取出任务信息提供Quartz。

添加任务的界面,我们需要给出这样的信息以及咱们自己编写的任务子类的dll,我们在这会将dll保存到Quartz服务节点和Web站点中。任务类全名是命名空间加上子类的名字。其他都是Quartz文档上都是会介绍的。程序集名称和上传的文件匹配。具体代码如下:

[HttpPost]

        /// <summary>

        /// 添加任务

        /// </summary>

        /// <param name="jobName">任务名称</param>

        /// <param name="jobGroupName">任务所在组名称</param>

        /// <param name="triggerName">触发器名称</param>

        /// <param name="triggerGroupName">触发器所在的组名称</param>

        /// <param name="cron">执行周期表达式</param>

        /// <param name="dllName">任务程序集名称(xxxx.dll)</param>

        /// <param name="fullJobName">任务类全名</param>

        /// <param name="jobDescription">任务描述</param>

        /// <param name="jobstartTime">开始时间</param>

        /// <returns></returns>


        public JsonResult AddJob(string jobName, string jobGroupName, string triggerName, string triggerGroupName, string cron, string dllName, string fullJobName, string jobDescription, DateTime jobStartTime)

        {

            HttpPostedFileBase dllFile = Request.Files[0] as HttpPostedFileBase;

            if (dllFile == null || dllFile.ContentLength <= 0)

            {

                return Json(ResponseDataFactory.CreateAjaxResponseData("-10003", "无任务文件", null));

            }

            _savePathInWeb(dllFile);

            //TODO:添加到数据库自己的表

            var jobId = _customerJobInfoRepository.AddCustomerJobInfo(jobName, jobGroupName, triggerName, triggerGroupName, cron, dllName, fullJobName, jobDescription, jobStartTime);

            _savePathInRemoteServer(dllFile);

            return Json(ResponseDataFactory.CreateAjaxResponseData("1", "添加成功", jobId));


        }

添加完之后界面会变成如下这样:

可以看到这个任务可以执行运行和删除,运行周期是10秒钟执行一次。下面接着点击运行让这个任务跑起来:

这样这个任务就以咱们设置的运行周期运行起来,同时运行的任务提供了暂停,删除,更改运行周期操作:先看任务的运行效果,Quartz服务节点钟我们使用监听器对服务的监听同时输出了一些信息:

同时运行任务执行背后的执行逻辑为:

         [HttpPost]

        /// <summary>

        /// 启动任务

        /// </summary>

        /// <param name="jobId">任务编号</param>

        /// <returns></returns>

        public JsonResult RunJob(int jobId)

        {

            var ajaxResponseData = _operateJob(jobId, (jobDetail) => { jobDetail.TriggerState = 0; _customerJobInfoRepository.UpdateCustomerJobInfo(jobDetail); return _jobHelper.RunJob(jobDetail); });

            return Json(ajaxResponseData);

        }


       /// <summary>

        /// 运行任务

        /// </summary>

        /// <param name="jobInfo">任务信息</param>

        /// <returns></returns>

        public bool RunJob(Customer_JobInfo jobInfo)

        {

            Assembly assembly = Assembly.LoadFile(AppDomain.CurrentDomain.BaseDirectory + $"bin/{jobInfo.DLLName}");

            var type = assembly.GetType(jobInfo.FullJobName);

            JobKey jobKey = _createJobKey(jobInfo.JobName, jobInfo.JobGroupName);

            if (!_scheduler.CheckExists(jobKey))

            {

                IJobDetail job = JobBuilder.Create(type)

                    .WithIdentity(jobKey)

                    .UsingJobData(_createJobDataMap("jobId", jobInfo.Id))

                    .Build();


                CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.CronSchedule(jobInfo.Cron);

                ITrigger trigger = TriggerBuilder.Create().StartNow()//StartAt(DateTime.SpecifyKind(jobInfo.JobStartTime, DateTimeKind.Local))

                    .WithIdentity(jobInfo.TriggerName, jobInfo.TriggerGroupName)

                    .ForJob(jobKey)

                    .WithSchedule(scheduleBuilder.WithMisfireHandlingInstructionFireAndProceed())

                    .Build();

          


                _scheduler.ScheduleJob(job, trigger);


            }

            return true;

        }

这里就用了到了我们第一步添加任务时上传的dll和任务类的全名 我们通过加载程序集通过全名找到我们需要执行的任务类,同时假如额外信息,JobId,以便监听器可以使用id更新自己的表。同样我们可以更改运行周期来调节任务调度的频次:

那么在更改运行周期之后我们需要重新构建触发器

/// <summary>

        /// 更改任务运行周期

        /// </summary>

        /// <param name="jobInfo">任务信息</param>

        /// <returns></returns>

        public bool ModifyJobCron(Customer_JobInfo jobInfo)

        {

            CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.CronSchedule(jobInfo.Cron);

            var triggerKey = _createTriggerKey(jobInfo.TriggerName, jobInfo.TriggerGroupName);

            ITrigger trigger = TriggerBuilder.Create().StartAt(DateTime.SpecifyKind(jobInfo.JobStartTime, DateTimeKind.Local))

                    .WithIdentity(jobInfo.TriggerName, jobInfo.TriggerGroupName)

                   .WithSchedule(scheduleBuilder.WithMisfireHandlingInstructionDoNothing())

                    .Build();

            _scheduler.RescheduleJob(triggerKey, trigger);

            return true;

        }

这个点是需要注意的!!!

接下来我们使用暂停功能:

暂停列表中我们对此任务可进行恢复和删除操作,那么恢复操作就是恢复任务运行,删除就是从调度器中删除这个任务同时我们将自己的表中标记删除。

到现在我们的整体流程就已经走了,但是最终我们的主题是分布式,当然这个已经能满足一部分需求了,所以接下来由于环境有限我这边会启动两个服务节点能模拟多个服务器然后我们关闭掉一个服务节点看另一个是否能拿到任务进行,启动任务后我们可以看到:

当我们关闭刚才正在调度此任务的调度器之后看看另一个调度器的结果:

可以看到 另一个调度器执行刚才那个任务,原理是 我们在服务端设置了检查时间每个调度器会以这个时间去数据库钟检测另外的节点是否正常,当挂掉后会从数据库钟取出任务信息再次调度。至此我们的分布式调度平台就大致分享完了。由于是第一版,所以很多地方还不合理或者需要改进会在之后的时间磨合中不断去完善。

最后定时框架有很多,大家要的是 第一符合自己的业务,第二自己能熟悉掌控的去选择技术。

上述代码已经放在github: https://github.com/NeverGiveUpChange/TaskManagerByQuartz.Net

相关文章:

  • 基于Quartz.net 的开源任务管理平台

  • 使用sqlserver搭建高可用双机热备的Quartz集群部署【附源码】

  • 开源任务管理平台TaskManager介绍

原文地址:http://www.cnblogs.com/clly/p/7767271.html


.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/322231.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Sentinel(二十四)之Sentinel Dashboard中修改规则同步到ZooKeeper

转载自 Springboot使用Sentinel限流&#xff0c;集成zookeeper完成规则的持久化 上一篇简单介绍了sentinel限流的基本配置和使用&#xff0c;这一篇我们来稍微深入一点&#xff0c;看看如何将zookeeper继承进来&#xff0c;用以保存添加的流控规则。 上一篇中我们启动了dash…

微软Azure AspNetCore微服务实战第2期(内附PPT下载)

2018年1月28日&#xff0c;虽然上海的大雪在城区已经见不到踪影&#xff0c;但还是很冷。不过天气再冷&#xff0c;也阻止不了小伙伴参加活动的热情。感谢王振&#xff0c;苏老师以及微软Azure API Management的产品经理Alvin&#xff0c;给大家带来微服务实战&#xff0c;企业…

jzoj3379-查询【主席树】

正题 题目大意 给出一个有序集合AAA&#xff0c;定义Al,rA_{l,r}Al,r​表示集合内l∼rl\sim rl∼r这个范围内的数。 定义加法ABABAB表示两个集合中的所有元素(不去重)。 现在询问&#xff0c;每次询问ki,pik_i,p_iki​,pi​然后给出kik_iki​个区间[lj,rj][l_j,r_j][lj​,rj…

Sentinel(二十五)之Sentinel Dashboard同步Apollo存储规则

转载自 Spring Cloud Alibaba基础教程&#xff1a;Sentinel Dashboard同步Apollo存储规则 在之前的两篇教程中我们分别介绍了如何将Sentinel的限流规则存储到Nacos和Apollo中。同时&#xff0c;在文末的思考中&#xff0c;我都指出了这两套整合方案都存在一个不足之处&#…

为什么选择.NETCore?

为什么选择.NETCore&#xff1f;在开展话题之前先出一张ASP.NETCore VS Node.js的性能对比图 ASP.NET Core VS node.js&#xff1a;继续正文&#xff1a;学习新的开发框架是一项巨大的投资。您需要学习如何在新框架中编写&#xff0c;构建&#xff0c;测试&#xff0c;部署…

欢乐纪中某A组赛【2019.7.10】

前言 好烦我最后写对了T1T1T1的808080分结果交错题导致T2T2T2的404040分没了T1T1T1也没拿多那些分。 话说好像ZDYZDYZDY比我还惨 成绩 这里还是按OJOJOJ上的分数排名 JJJ表示初中&#xff0c;HHH表示高中后面加的是几年级 RankRankRankPersonPersonPersonScoreScoreScoreAAA…

Sentinel(二十六)之Sentinel Dashboard中修改规则同步到Nacos

转载自 Spring Cloud Alibaba基础教程&#xff1a;Sentinel Dashboard中修改规则同步到Nacos 上一篇我们介绍了如何通过改造Sentinel Dashboard来实现修改规则之后自动同步到Apollo。下面通过这篇&#xff0c;详细介绍当使用Nacos作为配置中心之后&#xff0c;如何实现Sentin…

用C#编写Linux守护进程

如果要在Red Hat Enterprise Linux上将.NET Core进程作为后台进程运行&#xff0c;则可以创建自定义systemd单元。今天我将为.NET Core编写两个自定义系统单元的例子。一个是运行.NET Core控制台应用程序的一种类型&#xff0c;另一个是运行ASP.NET Core Web应用程序的简单类型…

P4550-收集邮票【期望dp】

前题 ZYCdalaoZYCdalaoZYCdalao让我推这题&#xff0c;然后我只推出了fff的推导&#xff0c;我还是太菜了QVQQVQQVQ 正题 题目链接:https://www.luogu.org/problemnew/show/P4550 题目大意 nnn种&#xff0c;每次随机买一个邮票(每种的概率均等)&#xff0c;然后第kkk种要kkk元…

Nacos client SDK 订阅式请求坑

一、场景复现 &#xff08;1&#xff09;业务需求 在不同的nacos注册集群&#xff0c;不同的namespace&#xff0c;由页面发起查询nacos集群上注册的服务实例ip。 &#xff08;2&#xff09;故障现象 nacos集群配置推送push超时&#xff0c;查看nacos日志请求一直在20~30qp…

AWS Lambda现已支持.NET Core 2.0

Amazon宣称.NET Core 2.0现在已经支持AWS Lambda以及无服务器应用程序了。开发者们现在可以使用C#和.NET Core 2.0来为AWS Lambda编写代码和编写运行于AWS的无服务器应用程序了。自2017年2月Amazon开始了对C#编程语言的支持&#xff0c;这使得.NET开发者能够使用.NET Core 1.0运…

jzoj3236-矮人排队【权值线段树】

正题 题目链接:https://jzoj.net/senior/#main/show/3236 题目大意 一个序列两个操作 1XY:1\ X\ Y:1 X Y:交换XXX和YYY两个数2AB:2\ A\ B:2 A B:询问A∼BA\sim BA∼B这些数再序列中是否是连续的一段区间(不一定按顺序) 解题思路 先不考虑交换&#xff0c;对于询问我们发现它…

Nacos(一)之简介

转载自 什么是 Nacos Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集&#xff0c;帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。 Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。 Nacos 是构建以“服务”为中心的现…

开源纯C#工控网关+组态软件(八)表达式编译器

一、 引子监控画面的主要功能之一就是跟踪下位机变量变化&#xff0c;并将这些变化展现为动画。大部分时候&#xff0c;界面上一个图元组件的某个状态&#xff0c;与单一变量Tag绑定&#xff0c;比如电机的运行态&#xff0c;绑定一个MotorRunning信号&#xff1b;但有些时候…

jzoj3237-间谍派遣【最小生成树,并查集】

正题 题目大意 一张图&#xff0c;第iii个点参加任务需要mkimk_imki​元&#xff0c;连接一条边需要一定费用&#xff0c;要求每个联通图都有参加任务的点&#xff0c;求最小费用。 解题思路 其实就是求若干个最小生成树然后这个最小生成树的权值就是这个棵树的所有边权加上最…

g4e基础篇#6 了解Git历史记录

Git的版本历史记录采用了与传统集中式版本管理系统完全不同的方式进行组织&#xff0c;在刚开始使用Git的时候我们往往会不知所措&#xff0c;比如看到这样的历史记录。看到这个七拐八拐的图形&#xff0c;你可能完全不知道它代表了什么。其实这正是Git的特别之处&#xff0c;G…

Nacos(二)之概念

转载自 Nacos 概念 NOTE: Nacos 引入了一些基本的概念&#xff0c;系统性的了解一下这些概念可以帮助您更好的理解和正确的使用 Nacos 产品。 地域 物理的数据中心&#xff0c;资源创建成功后不能更换。 可用区 同一地域内&#xff0c;电力和网络互相独立的物理区域。同一可…

bzoj3482,jzoj3238-超时空旅行hiperprostor【最短路,凸包,斜率优化】

正题 题目链接:https://www.lydsy.com/JudgeOnline/problem.php?id3482 题目大意 一张有向图有正整数边权也有xxx边权。其中xxx可以取任何值(但是要注意所有的xxx边必须长度相等)&#xff0c;每次询问求SSS到TTT的可能最短路长度个数和它们的和。 解题思路 分层图&#xff…

Ray框架QA

Orleans与Akka对比&#xff0c;为什么选用Orleans&#xff1f;答: Akka对参与开发的人员要求更高一些&#xff0c;普遍是专家级别&#xff0c;Orleans框架进一步抽象了一层&#xff0c;结合C#语言特性&#xff0c;能普遍降低开发难度。下面是知乎网友的答案&#xff0c;可以参考…

Nacos(三)之架构

转载自 Nacos 架构 基本架构及概念 服务 (Service) 服务是指一个或一组软件功能&#xff08;例如特定信息的检索或一组操作的执行&#xff09;&#xff0c;其目的是不同的客户端可以为不同的目的重用&#xff08;例如通过跨进程的网络调用&#xff09;。Nacos 支持主流的服务…