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

因爲日誌用的公司本身的,沒去改它,因此下載下來要報錯,手動換一下就能夠了

相關文章
相關標籤/搜索