偶然发现项目中有多个需要定时完成的任务,通过以下方式进行的。
- 有一些是写在MSSql Server的作业中,这些是纯粹滴操作数据库的行为。
- 有一些是写在Window Service中,用Timer定时器来轮询完成。既包括操作数据库,又包括发邮件,或者同步数据去其他系统。
但是这些都1对1的,在SqlServer中,一个作业完成一件事件。有多个作业那就必须建立多个作业。需要移植到其他数据库就必须把每个作业的Sql复制过去执行,很繁琐。
在Window Service中,也是1个任务对于1个服务,若有多个任务就建立多个服务。若要移植到其他机器,就得一个一个去安装这些服务。
最重要的是,你都不清楚到底有多少Window Service是这个项目的,有多少作业在MSSql Server中运行。管理相当不方便。
在可移植性方面,确实非常不方便。也不便于统一管理。头疼。
最近研究Quartz.net,发现它能很方便地统一调度多个任务。若是Web的项目,只需在Application_Start方法中加载这些任务,并开始调度。在Application_Stop方法中将调度Shutdown。若有更改任务,只需重启应用程序即可。
我进行了试验,将所有的任务放在配置文件中JobScheduler.config:
<?xml version="1.0" encoding="utf-8" ?>
<JobScheduler>
<Job Description="作业1">
<DllName>JobLibrary.dll</DllName>
<JobDetail job="test1" group="test1Group" jobtype="JobLibrary.FirstJob" />
<Trigger name="test1" group="test1Group" type="CronTrigger" expression="0/2 * * * * ?" />
</Job>
<Job Description="作业2">
<DllName>JobLibrary.dll</DllName>
<JobDetail job="test2" group="test2Group" jobtype="JobLibrary.SecondJob" />
<Trigger name="test2" group="test2Group" type="CronTrigger" expression="0/5 * * * * ?" />
</Job>
<Job Description="作业3">
<DllName>JobLibrary.dll</DllName>
<JobDetail job="test3" group="test3Group" jobtype="JobLibrary.ThirdJob" />
<Trigger name="test3" group="test3Group" type="CronTrigger" expression="0/3 * * * * ?" />
</Job>
</JobScheduler>
统一调度的主程序:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using Quartz.Impl;
using Quartz;
using Quartz.Impl.Triggers;
using System.Reflection;
using System.IO;
using Common.Logging;
namespace JobLibrary
{
public class JobManage
{
private static ISchedulerFactory sf = new StdSchedulerFactory();
private static IScheduler scheduler;
static readonly ILog errorLog = LogManager.GetLogger("LogFileAppender");
public static void StartScheduleFromConfig()
{
string currentDir = AppDomain.CurrentDomain.BaseDirectory;
try
{
XDocument xDoc = XDocument.Load(Path.Combine(currentDir, "JobScheduler.config"));
var jobScheduler = from x in xDoc.Descendants("JobScheduler") select x;
var jobs = jobScheduler.Elements("Job");
XElement jobDetailXElement, triggerXElement;
scheduler = sf.GetScheduler();
CronTriggerImpl cronTrigger;
foreach (var job in jobs)
{
//加载程序集joblibaray
Assembly ass = Assembly.LoadFrom(Path.Combine(currentDir, job.Element("DllName").Value));
jobDetailXElement = job.Element("JobDetail");
triggerXElement = job.Element("Trigger");
JobDetailImpl jobDetail = new JobDetailImpl(jobDetailXElement.Attribute("job").Value,
jobDetailXElement.Attribute("group").Value,
ass.GetType(jobDetailXElement.Attribute("jobtype").Value));
if (triggerXElement.Attribute("type").Value.Equals("CronTrigger"))
{
cronTrigger = new CronTriggerImpl(triggerXElement.Attribute("name").Value,
triggerXElement.Attribute("group").Value,
triggerXElement.Attribute("expression").Value);
scheduler.ScheduleJob(jobDetail, cronTrigger);
}
}
scheduler.Start();
}
catch (Exception e)
{
errorLog.Error(e.StackTrace);
}
}
public static void ShutDown()
{
if (scheduler != null && !scheduler.IsShutdown)
{
scheduler.Shutdown();
}
}
/**
* 从Scheduler 移除当前的Job,修改Trigger
*
* @param jobDetail
* @param time
* @throws SchedulerException
* @throws ParseException
*/
public static void ModifyJobTime(IJobExecutionContext jobExecution, String time)
{
scheduler = jobExecution.Scheduler;
ITrigger trigger = jobExecution.Trigger;
IJobDetail jobDetail = jobExecution.JobDetail;
if (trigger != null)
{
CronTriggerImpl ct = (CronTriggerImpl)trigger;
// 移除当前进程的Job
scheduler.DeleteJob(jobDetail.Key);
// 修改Trigger
ct.CronExpressionString = time;
Console.WriteLine("CronTrigger getName " + ct.JobName);
// 重新调度jobDetail
scheduler.ScheduleJob(jobDetail, ct);
}
}
}
}
以下是3个Job:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Common.Logging;
namespace JobLibrary
{
public abstract class JobBase
{
/// <summary>
/// JOB状态日志
/// </summary>
protected internal static readonly ILog jobStatus = LogManager.GetLogger("LogFileAppender");
/// <summary>
/// 服务错误日志
/// </summary>
protected internal static readonly ILog serviceErrorLog = LogManager.GetLogger("LogFileAppender");
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Quartz;
namespace JobLibrary
{
public class FirstJob : JobBase, IJob
{
#region IJob 成员
public void Execute(IJobExecutionContext context)
{
jobStatus.Info("--------1111first job start ----------");
try
{
jobTest();
}
catch (Exception e)
{
serviceErrorLog.Info(string.Concat("first job:", e.StackTrace));
}
jobStatus.Info("---------first job Complete ----------");
}
#endregion
public void jobTest()
{
jobStatus.Info("weeksetting test1!");
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Quartz;
namespace JobLibrary
{
public class SecondJob:JobBase,IJob
{
#region IJob 成员
public void Execute(IJobExecutionContext context)
{
jobStatus.Info("------------2222 second job start -----------");
try
{
jobTest();
}
catch (Exception e)
{
serviceErrorLog.Info(String.Concat("second job:", e.StackTrace));
}
jobStatus.Info("------------ second job Complete -----------");
}
#endregion
public void jobTest()
{
jobStatus.Info("weeksetting test2!");
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Quartz;
namespace JobLibrary
{
public class ThirdJob : JobBase, IJob
{
private static int a = 1;
#region IJob 成员
public void Execute(IJobExecutionContext context)
{
jobStatus.Info("--------3333Third job start ----------");
try
{
if (!context.JobDetail.JobDataMap.Contains("a"))
{
context.JobDetail.JobDataMap.Add("a", a);
}
else
{
context.JobDetail.JobDataMap["a"] = a;
}
jobTest();
jobStatus.Info("a=" + context.JobDetail.JobDataMap["a"]);
if (a == 3)
{
JobManage.ModifyJobTime(context, "0/5 * * * * ?");
}
jobStatus.Info("a=" + a);
a++;
}
catch (Exception e)
{
serviceErrorLog.Info(string.Concat("Third job:", e.StackTrace));
}
jobStatus.Info("---------Third job Complete ----------");
}
#endregion
public void jobTest()
{
jobStatus.Info("weeksetting test3!");
}
}
}
以下是2个配置文件App.config,log4net.config,用于配置Log的:
App.config
<?xml version="1.0"?>
<configuration>
<configSections>
<sectionGroup name="common">
<section name="logging" type="Common.Logging.ConfigurationSectionHandler, Common.Logging"/>
</sectionGroup>
</configSections>
<common>
<logging>
<!--1.此Adapter只输出到控制台--><!--
<factoryAdapter type="Common.Logging.Simple.ConsoleOutLoggerFactoryAdapter, Common.Logging">
<arg key="level" value="INFO" />
<arg key="showLogName" value="true" />
<arg key="showDataTime" value="true" />
<arg key="dateTimeFormat" value="yyyy/MM/dd HH:mm:ss:fff" />
</factoryAdapter>-->
<!--2.此Adapter只输出到Log4.net的配置文件所指定的地方-->
<factoryAdapter type="Common.Logging.Log4Net.Log4NetLoggerFactoryAdapter, Common.Logging.Log4net">
<arg key="configType" value="FILE"/>
FILE,FILE-WATCH,INLINE,EXTERNAL
<arg key="configFile" value="~/log4net.config"/> <!-- 指定log4net的配置文件名称 -->
<arg key="level" value="Warn"/>
</factoryAdapter>
</logging>
</common>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Common.Logging" publicKeyToken="AF08829B84F0328E" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-2.1.2.0" newVersion="2.1.2.0"/>
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
log4net.config
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />
</configSections>
<appSettings>
</appSettings>
<log4net>
<!--定义输出到文件中-->
<appender name="LogFileAppender" type="log4net.Appender.RollingFileAppender">
<!--输出日志文件的路径-->
<file value="Log\XXLog.log" />
<!--输出日志时自动向后追加-->
<appendToFile value="true" />
<!--防止多线程时不能写Log,官方说线程非安全,但实际使用时,本地测试正常,部署后有不能写日志的情况-->
<lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
<!--置为true,当前最新日志文件名永远为file节中的名字-->
<staticLogFileName value="false" />
<!--日志以大小作为备份样式,还有一种方式是Date(日期)-->
<rollingStyle value="size" />
<countDirection value="-1" />
<!--单个日志的最大容量,(可用的单位:KB|MB|GB)不要使用小数,否则会一直写入当前日志-->
<maximumFileSize value="1MB" />
<!--日志最大个数,都是最新的-->
<maxSizeRollBackups value="10" />
<datePattern value='"."yyyy-MM-dd".log"' />
<layout type="log4net.Layout.PatternLayout">
<!--每条日志末尾的文字说明-->
<footer value="**************************************************************" />
<!--输出格式-->
<!--样例:2008-03-26 13:42:32,111 [10] INFO Log4NetDemo.MainClass - info-->
<conversionPattern value="%newline%d{yyyy/MM/dd,HH:mm:ss.fff},[%-5level]%newline Message:%message%newline" />
</layout>
</appender>
<root>
<!--文件形式记录日志-->
<appender-ref ref="LogFileAppender" />
</root>
</log4net>
</configuration>
使用时,在Global.asax.cs中增加这样:
protected void Application_Start(object sender, EventArgs e)
{
JobManage.StartScheduleFromConfig();
}
protected void Application_End(object sender, EventArgs e)
{
JobManage.Shutdown();
}
如有新的任务加入,只需在JobScheduler.config中配置新任务项。若要停止某个任务,只需注释掉任务项,然后重启应用程序
application改变的条件:
1. iis重启
2. 代码改变
3. 应用程序池回收时间到了
4. 应用程序池空闲时间到了
符合以上条件后,第一个request过来时会调用Application_Start。其中3和4是可以配置的(应用程序池属性 -> 性能)
把所有的Job、JobManage主调度程序都放到一个工程JobLibrary,这样便于管理,一个Job对于一个类。要移植时,只需把该类库搬走。