Quartz.NET 3.0.7 + MySql 動態調度作業+動態切換版本+多作業引用同一程序集不同版本+持久化+集群(一)
Quartz.NET 3.0.7 + MySql 動態調度作業+動態切換版本+多作業引用同一程序集不同版本+持久化+集群(三)
Quartz.NET 3.0.7 + MySql 動態調度作業+動態切換版本+多作業引用同一程序集不同版本+持久化+集群(四)
上篇文章搞定了第一個功能.
1.利用反射動態創建Job;
2.調度服務如何知道有新的任務來了?是調度服務輪詢數據庫?還是管理后台通知調度服務?又或者遠程代理?
3.需要一個管理后台,提供啟動,暫停,恢復,停止等功能;
4.至於集群,Quartz.NET 本身就提供該功能,只不過要使用它的持久化方案而已.這個點只需要在配置文件上做做手腳就可以了,並不需要怎么開發.
5.管理后台如何實現啟動,暫停,恢復,停止等功能?靠遠程代理?還是通過其他方式?
接下來解決剩下的問題.
我一直認為世間萬物,塵歸塵,土歸土,本質都是一樣的.
動物與動物交流,機器與機器交流,兩個應用程序之間的交流,管你是什么東西交流,都跟人與人交流一樣.
要么你不停的問他,要么你等他告訴你.
再不濟,你倆都看對方不順眼,不想彼此直接交流,於是找來一個中間人.
他告訴中間人,中間人告訴你,
或者中間人不停的問他,有了消息,中間人再告訴你.
又或者你想要什么消息了,就去問中間人.中間人告訴你沒有,那就沒有.中間人說"我找一找","誒,這里有.來給你消息"
我認為其實就是這么回事兒,當然,我入行不久,理解還不夠深入.不過目前我覺得這樣理解能解決問題,就夠了.
學習講究的是方法,一來就研究到最底層,不是明智之舉.等哪天發現這么理解不能解決問題,這么理解有問題的時候,再深入研究也不遲.
還是那句話,路要一步一步走,飯要一口一口吃.存在的就是合理的.
當我們在管理后台新增一個作業的時候,作業的信息,比如名稱,時間表達式,程序集物理路徑,作業類型的完全限定名等,我們肯定是要找張表單獨存起來的,所以這里需要新建一張表:

CREATE TABLE `jobinfo` ( `Id` int(11) NOT NULL AUTO_INCREMENT COMMENT '編號', `SchedName` varchar(20) CHARACTER SET utf8mb4 NOT NULL COMMENT '調度器名稱', `JobName` varchar(50) CHARACTER SET utf8mb4 NOT NULL DEFAULT '' COMMENT '作業名稱', `JobGroup` varchar(20) CHARACTER SET utf8mb4 NOT NULL DEFAULT '' COMMENT '作業組', `Cron` varchar(50) CHARACTER SET utf8mb4 DEFAULT '' COMMENT '時間表達式', `Second` int(11) NOT NULL DEFAULT '0' COMMENT '間隔時間,單位:秒', `AssemblyPath` varchar(250) CHARACTER SET utf8mb4 NOT NULL DEFAULT '' COMMENT '作業程序集物理路徑', `ClassType` varchar(100) CHARACTER SET utf8mb4 NOT NULL DEFAULT '' COMMENT '作業完全限定名', `StartTime` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT '作業開始時間', `CreateTime` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT '作業創建時間', `ProjectTeam` varchar(20) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '項目組', `IsDeleted` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否刪除 0:否 1:是', PRIMARY KEY (`Id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
那么這些數據如何讓調度服務知道呢?(由於調研 Quartz,NET 框架的時候,看到很多大神說,IIS 回收池有坑.所以我就沒考慮把調度服務和管理后台集成在一起)
我第一版做的輪詢 ,就是在調度服務啟動的時候,啟動一個預先已經建好的輪詢job(輪詢的間隔時間盡量短一點,調低啞火忍耐時間,設置好失火策略),
輪詢job掃描這張表(下簡稱:作業表),然后根據表里的某個字段來判斷是否已經啟動了該job.
同時,當Job監聽器監聽到本次輪詢job執行完成后,暫停它,避免無謂的輪詢.
當管理后台新增了一個作業時,就通過遠程代理對象恢復該輪詢job.
我自以為這個方案很牛B,或者有那么一點點小"聰明".
但是,我后來把這個方案干掉了.
因為要使用遠程代理,管理后台就必須要 安裝 Quartz.NET ,這一點我感覺很不爽.我只想在調度服務一個地方安裝它.
這里插一句.
為了實現遠程代理,網上找了好多代碼,各種配置,都失敗了,不知道是我沒copy對,還是版本問題.
這里奉上我自己研究出來的,實測可用的代碼.至於遠程代理的配置文件,就不貼出來了,網上太多了.
RemotingSchedulerProxyFactory proxyFactory = new RemotingSchedulerProxyFactory
{
Address = "tcp://127.0.0.1:555/QuartzScheduler" }; var schedulerProxy = proxyFactory.GetProxy();
第二版,也就是目前采用的方案:
在調度服務內利用 owinself 組件內置一個api接口,接收管理后台的請求,拿到作業的數據后,實現該作業的啟動,暫停,恢復,停止等操作.
因此,管理后台不需要安裝 Quartz.NET 組件了,只需要操作一下作業表,把作業的信息post給調度服務內置的api接口即可.
整個設計如下:
(03 調度服務框架核心 中的 Middleware 大家可以不用理它,僅僅是我拿來練手用的)
引用關系如下:
Host 引用 Service ,Service 里面主要是初始化調度器,啟動API監聽方面的代碼;
Service 引用 Api , Api 接收管理后台的請求
Api 引用 Logic ,Logic 里面就是具體的對Job的啟動,暫停,恢復等操作了.
另外, Api , Logic 都需要應用 Model
Logic 還需要引用 BaseJob
管理后台與調度框架沒半毛錢聯系.(當然,Model還是要引用一下)
個人覺得這個設計耦合度比較低了.不過,還是那句話,任何事物都要辯證來看,耦合度是低了,開發量就相對多了一些.
是時候看看界面了,MVC做的,很清(jian)爽(lou)吧!我的水平實在有限,就這個界面我還是網上抄的模板,當然也參考了這位前輩對Quartz.NET使用方面的一些思路.原諒我,帖子找不到了....
像很多功能,比如觸發器的觸發機制選擇,執行次數,失火策略等,我就沒有在頁面上體現了,而是在調度框架內部暫時寫死了.一是時間來不及,二是公司的調度任務基本都差不多,沒有什么大的區別.不過以后肯定還是要加上.比如我只想今天下午執行10次等等
對這個界面做一個簡單的說明:
- 從"編號"到"程序集",這些字段的值來自作業表 jobInfo.
- "狀態","開始時間","上次執行","下次執行"4個字段來自官方的 qrtz_triggers 表.
- 頁面暫時不是實時的,要看最新狀態需要F5刷新.
對於"調度器名稱"字段需要特別說明.
整個框架開發到一半的時候,來了個新需求:
要同時開多個調度服務(控制台程序)調度不同的任務,但是用同一張數據庫表,同一個管理后台來管理.
比如現在已經啟了一個控制台程序了,管理了10個任務;
再啟一個控制台程序,管理另外10個任務,但是管理后台還是同一個,數據庫表還是同一張.注意,不是集群,只是想分開管理任務而已.
基於這個需求,所以設計了"調度器名稱"字段.
了解持久化方案的朋友肯定知道,在quartz.config配置文件中有這么一行:
quartz.scheduler.instanceName = wechat
我的方案就是利用這句配置,
一個控制台程序(宿主)就是一個調度器,同時,將api地址放到控制台程序的配置文件中:
<add key="ApiAddress" value="http://localhost:25250" />
當我們再開一個控制台程序時,(注意,不是集群),就需要同時修改上面兩個配置,
比如新的控制台程序的配置及新的quartz.config如下:
quartz.scheduler.instanceName = refuge <add key="ApiAddress" value="http://localhost:25251" />
那么,這時候管理后台就需要增加如下配置了:
<add key="wechat" value="http://localhost:25250" /> <add key="refuge" value="http://localhost:25251" />
當我們點擊按鈕,發送請求前,先根據這個job的 "調度器名稱" 字段從配置文件中獲取它應該請求的地址.
為了做到絕對安全,我在控制台程序的api中添加了過濾器,操作請求過來的時候,檢查傳過來的job數據中的"調度器名稱"是否和該控制台程序中的調度器名稱一樣.不一樣則不做任何操作.
效果:
既然說到這個份上了,順便把集群也說了.配置文件就不貼了,網上太多.
Quartz.NET 的集群功能到底是個什么功能?它實際覆蓋兩個功能:
- 解決單點問題
- 均衡服務器壓力
解決單點問題
簡單說就是啟動兩個控制台程序,作業只會被一個控制台程序調度,當其中一個掛了,另外一個立馬開始工作.
由於我這個框架內置了api,api監聽地址肯定不能重復,所以要使用集群,必須修改api地址.
那么在集群模式下,管理后台怎么知道應該請求哪個api呢?
我們先看看效果,然后再解釋.
假設現在有兩個控制台應用程序,配置如下,調度器名稱都叫 "wechat",並且本身已經存在一個Job了.
控制台1:
<add key="ApiAddress" value="http://localhost:25250" />
控制台2:
<add key="ApiAddress" value="http://localhost:25260" />
效果圖:
可以清楚的看到,下面的控制台程序並沒有執行Job,現在我們關掉上面的控制台,
我是20秒的時候關閉的,過了16秒,下面的控制台開始執行了.
現在來解釋下,管理后台的操作到底請求哪個api.
可能會有朋友認為,肯定要請求 25250 ,因為 25260 的控制台處於"備用"狀態,請求它沒效果.
事實上,這樣理解是錯的.
就算請求發送到 25260 控制台,雖然表面上這個控制台的調度器是處於"備用"狀態,但實際上它只是"待命"而已,有請求過來,它依然能"干活".
"改革春風吹滿地",實踐是檢驗真理的唯一標准.
還是上面兩個控制台,配置文件不變,現在修改一下管理后台的配置文件,api地址修改為 25260
<add key="wechat" value="http://localhost:25260" />
運行效果:
21:18:00左右的時候 ,我通過管理后台啟動了 Job2,可以看到 25260 所在的控制台開始干活了.
所以,根本不用擔心,以集群的方式運行多個宿主的時候,管理后台應該請求哪一個api,而事實上,我們應該在管理后台的配置文件中,把所有集群的api地址都寫上:
<add key="wechat" value="http://localhost:25250,http://localhost:25260" />
然后,請求的時候,判斷地址是否被監聽,只要被監聽了,post過去就不會有問題.
比如上面這個配置,如果某一天 25250 控制台掛了,無所謂,25260 不還在么?請求發送到 25260 就OK了.我們要做的僅僅是在請求前判斷一下這個地址是否已被監聽就行了,沒被監聽,就換一個.
均衡服務器壓力
這個就直接上圖吧!
下面來解釋一下:
首先,4個Job的時間表達式都一樣: 0/30 * * * * ?
我先啟動上面的控制台,在22:38:00 秒,4個Job都執行完成后,我啟動了下面的控制台,可以看到,"負載均衡"是起到了效果的.但是,4個Job在下面的控制台都各自重復了一次.
而且不管Job的間隔時間是多久,不過失火策略是什么,不管啞火的忍耐時間是多久,不管我隔多久啟動下面的控制台,我做了很多實驗,都會重復.而且遲早會重復一次,比如上面的 Job3 .但是只會重復一次.
這個有點小"坑"...我反復檢查了我的代碼,感覺不是代碼層面的問題.
我不知道這到底原因是什么?有沒有哪位前輩知道的?能否告知一下,或者大概可能是哪個方面的原因?
先到這吧!太晚了,明天繼續寫。
躺在床上,想起一個問題,判斷api地址是否被監聽不對!
我的集群是在兩個服務器上.我去......
我傻逼了......媽蛋......睡覺💤