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;
View Code

那么这些数据如何让调度服务知道呢?(由于调研 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地址是否被监听不对!

我的集群是在两个服务器上.我去......

我傻逼了......妈蛋......睡觉💤

posted @ 2019-01-27 23:17  热敷哥  阅读(2018)  评论(10编辑  收藏  举报