Quartz.NET 3.0.7 + MySql 动态调度作业+动态切换版本+多作业引用同一程序集不同版本+持久化+集群(二)
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地址是否被监听不对!
我的集群是在两个服务器上.我去......
我傻逼了......妈蛋......睡觉💤