原文:Quartz.NET 3.0.7 + MySql 实现动态调度作业+动态切换版本+多作业引用同一程序集不同版本+持久化+集群(一)

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

在生活中,"经验教训"常常一起出现,但在如今的快餐年代,太多人往往只关注经验,希望可以一步登天.

在巨人的肩膀上固然可以看得更高,更远,但任何事物都应该辩证的看.

经验固然可以让人走捷径,

但教训可以让人不走弯路.

希望这篇"心路历程"能让大家有所收获,也希望各位大佬留下宝贵意见.

需求

直接上领导的原话:

拆分需求

在博客园看了几篇 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 , 这是也我这个框架用的到类:

    public class JobCenter: IJob
{
public async Task Execute(IJobExecutionContext context)
{
await Task.FromResult(0);
}
}

第一个知识点:

我们在创建一个JobDetail 的时候,需要通过 WithIdentity 方法注册"名称"和"分组",如:

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

大家完全可以这样理解:

就当下面的红色代码被"某种"神秘力量隐藏了,而 WithIdentity("测试名称","测试组")   方法就相当于是实例化 JobCenter 类型时传入了两个入参.

上面的代码创建了一个 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)
{
//里面是具体的逻辑
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>
/// 将对象生存期更改为永久,因为默认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个问题被完全解决了...万里长征终于迈出了第一步!!!

Quartz.NET 3.0.7 + MySql 实现动态调度作业+动态切换版本+多作业引用同一程序集不同版本+持久化+集群(一)的更多相关文章

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

    把 HAProxy 用上了,终于不用担心某个节点挂了,还要去手动修改管理后台配置文件的api地址了. 在某网站下载了一个 window 可以用的版本 haproxy-1.7.8 不得不吐槽一下,作者要 ...

  2. Microsoft.Office.Interop.Excel, Version=12.0.0.0版本高于引用的程序集(已解决)

    Microsoft.Office.Interop.Excel, Version=12.0.0.0版本高于引用的程序集(已解决) 论坛里的帮助:http://bbs.csdn.net/topics/39 ...

  3. Redis集群环境使用的是redis4.0.x的版本,在用java客户端jedisCluster启动集群做数据处理时报java.lang.NumberFormatException: For input string: "7003@17003"问题解决

    java.lang.NumberFormatException: For input string: "7003@17003" at java.lang.NumberFormatE ...

  4. Net作业调度(四)—quartz.net持久化和集群

    介绍 在实际使用quartz.net中,持久化能保证实例重启后job不丢失. 集群能均衡服务器压力和解决单点问题. quartz.net在这两方面配置都比较简单. 持久化 quartz.net的持久化 ...

  5. quartz.net持久化和集群【转】

    在实际使用quartz.net中.持久化能保证实例重启后job不丢失. 集群能均衡服务器压力和解决单点问题. quartz.net在这二块配置都比较方便,来看下. 一:持久化 quartz.net的持 ...

  6. docker 下 mysql 集群的搭建

    下载程序&&创建docker容器 从mysql官网https://dev.mysql.com/downloads/cluster/上下载mysql集群库mysql-cluster-gp ...

  7. Dubbo入门到精通学习笔记(二十):MyCat在MySQL主从复制的基础上实现读写分离、MyCat 集群部署(HAProxy + MyCat)、MyCat 高可用负载均衡集群Keepalived

    文章目录 MyCat在MySQL主从复制的基础上实现读写分离 一.环境 二.依赖课程 三.MyCat 介绍 ( MyCat 官网:http://mycat.org.cn/ ) 四.MyCat 的安装 ...

  8. Python 检测系统时间,k8s版本,redis集群,etcd,mysql,ceph,kafka

    一.概述 线上有一套k8s集群,部署了很多应用.现在需要对一些基础服务做一些常规检测,比如: 系统时间,要求:k8s的每一个节点的时间,差值上下不超过2秒 k8s版本,要求:k8s的每一个节点的版本必 ...

  9. MySQL NDB集群安装配置(mysql cluster 9.4.13 installation)

    一.安装前规划 1.安装软件版本:mysql-cluster-gpl-7.4.13-linux-glibc2.5-x86_64.tar.gz 2.安装规划: 主机名 Ip地址 角色 db01 192. ...

随机推荐

  1. HDU 1007 Quoit Design 平面内最近点对

    http://acm.hdu.edu.cn/showproblem.php?pid=1007 上半年在人人上看到过这个题,当时就知道用分治但是没有仔细想... 今年多校又出了这个...于是学习了一下平 ...

  2. HTML5梦幻星空,可用作网页背景

    <html> <head> <title>星空</title> <META http-equiv="X-UA-Compatible&qu ...

  3. Java 函数的参数说

    java函数参数传递的到底是值还是引用对确实容易让人迷糊.而很多时候因为对这个问题的模糊甚至造成一些错误.最常见的说法是基本类型传的是值,对象传的引用.对于基本类型,大家都达成共识,没有什么可以争论的 ...

  4. 数据类型的提升(promotion)

    假如参与运算的数据类型不同或者取值范围过小,编译器会自动将其转换为相同的类型,这个类型就叫数据类型的提升(promotion). 1. C++ 语言环境的规定 unsigned char a = 17 ...

  5. JQuery 各节点获取函数:父节点,子节点,兄弟节点

    jQuery.parent(expr)           //找父元素 jQuery.parents(expr)          //找到所有祖先元素,不限于父元素 jQuery.children ...

  6. Java基础学习总结(40)——Java程序员最常用的8个Java日志框架

    作为一名Java程序员,我们开发了很多Java应用程序,包括桌面应用.WEB应用以及移动应用.然而日志系统是一个成熟Java应用所必不可少的,在开发和调试阶段,日志可以帮助我们更好更快地定位bug:在 ...

  7. Dynamics CRM 2016 Web API 消息列表

    Function Name Description CalculateTotalTimeIncident Function Calculates the total time, in minutes, ...

  8. JS实现动画方向切换效果(包括:撞墙反弹,暂停继续左右运动等)

    <!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8&quo ...

  9. unmapping error

    否则,会映射一个Getch的器件,就会报unmapping 的error

  10. MySQL字符编码问题,Incorrect string value

    MySQL上插入汉字时报错例如以下.详细见后面分析. Incorrect string value: '\xD0\xC2\xC8A\xBEW' for column 'ctnr' at row 1 M ...