Quartz.NET 3.0.7 + MySql 动态调度做业+动态切换版本+多做业引用同一程序集不一样版本+持久化+集群(一)

 

Quartz.NET 3.0.7 + MySql 动态调度做业+动态切换版本+多做业引用同一程序集不一样版本+持久化+集群(二)html

Quartz.NET 3.0.7 + MySql 动态调度做业+动态切换版本+多做业引用同一程序集不一样版本+持久化+集群(三)前端

Quartz.NET 3.0.7 + MySql 动态调度做业+动态切换版本+多做业引用同一程序集不一样版本+持久化+集群(四)git

 

 

前端时间,接到任务,写了一个调度框架.今天决定把心路历程记录在这里.作个记念.也方便提供给我这样的新手朋友,避免你们踩一样的坑.github

在生活中,"经验教训"经常一块儿出现,但在现在的快餐年代,太多人每每只关注经验,但愿能够一步登天.数据库

在巨人的肩膀上当然能够看得更高,更远,但任何事物都应该辩证的看.跨域

经验当然可让人走捷径,app

但教训可让人少走弯路.框架

但愿这篇"心路历程"能让你们有所收获,也但愿各位前辈留下宝贵意见.async

需求ide

拆分需求

在博客园看了几篇 Quartz.NET 的入门贴后,对该框架有了一个大体的了解.接下来就开始设计了.

正所谓路要一步一步走,饭要一口一口吃.

因而,我将需求划分红以下几个功能点和须要解决的问题:

1.利用反射动态建立Job;

2.调度服务如何知道有新的任务来了?是调度服务轮询数据库?仍是管理后台通知调度服务?又或者远程代理?

3.须要一个管理后台,提供启动,暂停,恢复,中止等功能;

4.至于集群,Quartz.NET 自己就提供该功能,只不过要使用它的持久化方案而已.这个点只须要在配置文件上作作手脚就能够了,并不须要怎么开发.

5.管理后台如何实现启动,暂停,恢复,中止等功能?靠远程代理?仍是经过其余方式?

开始干

要想经过 dll 方式灵活添加必然要用到反射.这点毋庸置疑.

Quartz.NET 建立一个Job的核心代码以下:

 IJobDetail jobDetail = JobBuilder.Create(typeof(Job)).Build()

同时,Job 类须要实现 IJob 接口,实现Execute() 方法.(关于 Quartz.NET 的基础知识本篇就不介绍了,博客园有不少前辈写了不少好文章)

那么,我只要能拿到 Type 不就完事儿了么?

这不 so easy.... 么

有新的调度任务了,就新建一个类库,Nuget 安装 Quartz.NET ,而后新建类,实现IJob接口,实现 Execute() 方法,调度服务里面反射加载程序集,拿到 type ,完事儿...

因而乎,我提笔就干,写下了以下代码:

       Assembly assembly = Assembly.LoadFile("程序集物理路径");
       Type type = assembly.GetType("类型彻底限定名");

至于调度服务怎么知道有新的调度任务来了,这个属于管理后台如何与调度服务通讯的问题,这个问题不是当前须要解决的,暂时放一边,后面再考虑.

上面代码写完后,测试了下,没问题,运行正常.

可是,问题来了.

1.我这个调度任务要切换版本怎么办?

2.我好几个调度任务引用了同一个程序集的不一样版本怎么办?

3.我这个调度任务里面要用本身的配置文件怎么办?

若是有朋友没有理解到上面这3个问题,我再举例说明一下:

第一个问题:

如今有两个调度任务

1. 类库项目 TestJob1.dll  ,定义了一个类型: Job1 ,其彻底限定名为 TestJob1.Job1

2. 类库项目 TestJob2.dll  ,定义了一个类型: Job2 ,其彻底限定名为 TestJob2.Job2

如今调度服务已经运行起来了,我经过某种方式通知到调度服务,而且已经成功反射加载了上述两个程序集.

若是这时候, TestJob1.dll 须要更新.怎么办?直接覆盖?不行的,会提示你:

 

"把调度服务关了,再覆盖"

这个能够有...

可是,我这个调度服务还管理者 Job2 ...实际工做中,可能有更多.为了更新某一个调度任务的版本就关闭整个调度服务,让全部的调度任务都停摆?Boss会砍死你的.

"个人调度任务都是天天凌晨运行,白天关一下没问题".------ What are you talking about ?

第二个问题:

一样以 TestJob1.dll 和 TestJob2.dll 举例.

假如这两个调度任务都引用了同一个程序集 Tools.dll ,可是版本不同.TestJob1.dll 引用 Tools.dll  v1.0.0.0 ,TestJob2.dll 引用的是 v1.0.0.1

那么若是反射加载 TestJob1.dll 和 TestJob2.dll 的时候到底会加载哪一个版本的 Tools.dll 呢?

谁先加载,就会加载谁引用的版本.

好比,若是先反射加载了TestJob1.dll ,那么会加载Tools.dll v1.0.0.0 .这时候再反射加载 TestJob2.dll 时,不会再加载 Tools.dll 了.

我曾奢望用什么骚操做能加载同一个程序集的不一样版本,或者说更新到高版本也行;最终以失败而了结.

因此,若是TestJob2.dll 用到了 v1.0.0.1 里面的新方法,那么很遗憾,调度服务运行时会报错,大概提示是:"未在程序集 Tools.dll v1.0.0.0 中找到方法 ......."

第三个问题:

依然以 TestJob1.dll 为例.

我在该类库项目中,新建应用程序配置文件:

<configuration>
  <appSettings>
    <add key="name" value="释放自我"/>
  </appSettings>
</configuration>
    public class Job1
    {
        public string Name = System.Configuration.ConfigurationManager.AppSettings["name"];
    }

你们以为反射后,建立的 Job1 的实例能拿到"释放自我"么?确定是拿不到了啦...除非你把配置写在 调度服务 的配置文件中..可是不可能我每加一个调度任务,都去调度服务的配置文件中添加配置吧..并且还有可能重名.固然,你要用File读取,当我没说...

那么,能不能在程序集用的时候加载它,用完就卸载.再用的时候再引用呢?

这时候,我想到<CLR via C#  第4版>这本书提到过:

"程序集加载后不能卸载,只能经过卸载 AppDomain 来卸载程序集".

因而乎,我翻开 <CLR via C#  第4版> ,依葫芦画瓢,天真而充满自信的写出以下代码:    

TestJob1.dll :

    public class Job1 : MarshalByRefObject, IJob
    {    public Task Execute(IJobExecutionContext context)
        {
            Console.WriteLine("我不会写PPT,只会干活");
            return Task.FromResult(0);
        }
    }

 

TestConsole.exe (调度服务):

            string assemblyPath = @"H:\0开发项目\Go.Job.QuartzNET\TestJob1\bin\1\TestJob1.dll";
            AppDomainSetup setup = new AppDomainSetup();
            setup.ShadowCopyFiles = "true";//这句话很是重要,核心中的核心,没有它,就算跨域也没有价值.这句代码的效果是:你看到的程序集并非正在用的程序集.用的是 它们的 Shadow.
            setup.ApplicationBase = System.IO.Path.GetDirectoryName(assemblyPath);          
       AppDomain appDomain = AppDomain.CreateDomain("newDomain", null, setup);

            object job = appDomain.CreateInstanceFromAndUnwrap(assemblyPath, "TestJob1.Job1");
            Type type = job.GetType();

            IScheduler scheduler = StdSchedulerFactory.GetDefaultScheduler().Result;
            scheduler.Start();

            IJobDetail jobDetail = JobBuilder.Create(type).WithIdentity("job1", "job1").Build();

            ITrigger trigger = TriggerBuilder.Create()
                .WithIdentity("trigger1", "trigger1")
                .WithSimpleSchedule(s => s.WithIntervalInSeconds(3)
                    .RepeatForever()).StartNow()
                .Build();

            scheduler.ScheduleJob(jobDetail, trigger);

结果运行报错,错在这一行: 

 

注意看 type ,是 MarshalByRefObject 类型.这类型,让 Quartz.NET 怎么建立 JobDetail...

因而,我又稍微改了改,让 调度服务 添加 TestJob1.dll 的引用

同时,跨域按"引用"封送过来后,强转为 Job1:

运行,没毛病...

修改一下Job1,复制一下,看会不会报错,竟然OK了,没有像上面提到的第一个问题那样,报下面这个错误.

这意味这代码能够在不关闭 调度服务的状况切换版本了...

可是仔细一想,不对啊! 调度服务运行起来后,我怎么添加引用......再说了,我怎么知道要转成哪一个 Job 类型?

这时候,一句名言涌上心头:

凡是能用技术问题解决的问题,均可以经过包一层来解决.

因而乎我改了一下代码:

新建了一个BaseJob类库,经过 Nuget 安装 Quartz.NET

三个类库的引用关系为:

TestConsole(调度服务)引用 BaseJob,二者都须要安装 Quartz.NET

TestJob1 引用 BaseJob.

TestConsole 没有引用 TestJob1

 

    public abstract class BaseJob : MarshalByRefObject, IJob
    {
        public Task Execute(IJobExecutionContext context)
        {
            Run();
            return Task.FromResult(0);
        }
        protected abstract void Run();
    }

 

    public class Job1 : BaseJob.BaseJob
    {
        protected override void Run()
        {
            Console.WriteLine("版本1");
        }
    }

调度服务中,跨域按"引用"封送后强转成 BaseJob 

 

运行一下,看看效果: 

确定是扯蛋的嘛!

调度服务都没有引用 TestJob1 怎么可能拿获得 Job1 的 Type,拿到的 Type 只会是 BaseJob

什么?关闭调度服务,把 TestJob1.dll copy到 调度服务的运行目录下.嗯,这个方法能解决问题.

可是,我想说一句:

"what are you talking about"

我完全懵逼了......

长时间的挣扎后,终于,在博客园找到一位大神2年前的一篇文章:https://www.cnblogs.com/zhuzhiyuan/p/6180870.html

当时看了不到几行,"做业管理(运行)池" 几个字简直让我醍醐灌顶!!!

至于后面的故事,你们能够看大神的文章了......

不过我这里仍是继续写,算是对本身开发过程的一个总结.

要用"池"的概念,就必须提到Quartz.NET 框架的两个知识点:

为了更好的理解,咱们先新建一个 JobCenter , 它是整个调度器执行Job的惟一入口,是也我这个框架用的到类:

  public class JobCenter: IJob
    {
        public async Task Execute(IJobExecutionContext context)
        {       
       //在Job池中,找到当前 JobCenter 对象的逻辑Job
       //执行逻辑Job
       await Task.FromResult(0);
     }
  }

第一个知识点:

咱们在建立一个JobDetail 的时候,须要经过 WithIdentity 方法注册"名称"和"分组",如:

 IJobDetail jobDetail = JobBuilder.Create<JobCenter>()
                .WithIdentity("测试名称","测试组")            
                .Build();

这段代码并无真正的建立一个 JobCenter 的实例.而只是注册了"身份"而已.

真正建立 JobCenter 的实例是在每次触发器触发的时候.

也就是说,触发器每次触发时,都会建立一个 JobCenter 的实例!!!这点很重要!!

因此,并非说定义了一个 JobCenter 类型,那它就是一个 JobDetail 了.

Job 和 JobDetail 的关系 == 类型 和 类型实例 的关系

JobDetail 的身份信息 == 类型的构造函数的两个入参.

你们彻底能够这样理解:

就当下面的红色代码被"某种"神秘力量隐藏了,每次建立一个 JobCenter 的实例时,都将注册的身份信息:"测试名称"和"测试组"传给了构造函数.

    public class JobCenter : IJob
    {
        private readonly string _name;
        private readonly string _group;

        public JobCenter(string name, string group)
        {
            _name = name;
            _group = group;
        }

        public async Task Execute(IJobExecutionContext context)
        {
            //在Job池中,找到当前 JobCenter 对象的逻辑Job
       //执行逻辑Job
await Task.FromResult(0); } }

第二个知识点:

咱们建立一个 JobDetail 的时候,是能够经过 SetJobData(...) 方法来保存数据的,好比红色部分:

       var data = new Dictionary<string, object>()
                  {                    
                      ["jobInfo"] = new JobInfo()//JobInfo 这个类后面会讲到       };

            IJobDetail jobDetail = JobBuilder.Create<JobCenter>()
                .WithIdentity("测试名称","测试组")
 .SetJobData(new JobDataMap(data))
                .Build();

这两个知识点 + 做业池+跨 AppDomain 按"引用"封送就构成了整个框架的核心.

因为定义了一个JobCenter,而且用到了池,因此 BaseJob 也不须要继承 IJob 了:

    /// <summary>
    /// 逻辑Job基类
    /// </summary>
    public abstract class BaseJob : MarshalByRefObject
    {   /// <summary>
        /// 具体逻辑
        /// </summary>
        protected abstract void Execute();

        /// <summary>
        /// 将对象生存期更改成永久,由于CLR默认5分钟内没有经过代理发出调用,对象会实效,下次垃圾回收会释放它的内存.
        /// </summary>
        /// <returns></returns>
        public override object InitializeLifetimeService()
        {
            return null;
        }
    }

核心伪代码:

    //定义一个Job池.
    //在建立 jobDetail 前,先建立逻辑job,即经过跨域按"引用"封送,拿到逻辑job的代理对象的引用.
   //而后在建立 jobDetail 的时候,将该 jobDetail 的信息 jobInfo 存入 JobDataMap 永久保存起来.
//同时,将该 jobDetail 执行时所要正真调用的 逻辑job(也就是 BaseJob 的子类)信息存入 job 池. //trigger 触发时, //从该 jobDetail 保存的数据中取出 jobInfo //根据 jobInfo 从 job 池中查找 对应的 逻辑job. //调用 逻辑job 的 Execute()方法执行具体逻辑. 再简单讲就是,触发器触发一个做业时,做业先去做业池找到属于它本身的逻辑做业,而后再执行逻辑做业.

这里提早讲一点:

做业池是在内存中,若是宕机是会丢失的;

而 JobDetail 和 Trigger 的数据都是在数据库中,不会丢失.(框架采用了官方的持久化方案).

因此须要写代码来处理这种意外状况.

终于...上面提到的3个问题被彻底解决了...万里长征终于迈出了第一步!!!

 

源码:https://github.com/wjire/Go.Job.QuartzNET3X

因为日志用的公司本身的,没去改它,因此下载下来要报错,手动换一下就能够了

相关文章
相关标签/搜索