<connectionStrings>
<add name="SportDb" connectionString="Data Source=.;Initial Catalog=SmartTrainTest;User ID=sa;Password=sa.;MultipleActiveResultSets=true;" providerName="System.Data.SqlClient" />
connectionStrings>
<appSettings>
<add key="OrgOrder" value="30" />
appSettings>
数据库连接管理类
public class DataBaseHelper
{
private static readonly string ConnectionSqlserver = ConfigurationManager.ConnectionStrings["SportDb"].ConnectionString;
public static SqlConnection DepperSqlserver()
{
SqlConnection connection = new SqlConnection(ConnectionSqlserver);
connection.Open();
return connection;
}
}
订单过期配置
public class ExpireConfig
{
//半小时
public static readonly long OrgOrderExpire = Convert.ToInt32(ConfigurationManager.AppSettings["OrgOrder"]);
}
数据库访问帮助类
public class DapperHelper
{
public static List Select(string sql, object param)
{
using (IDbConnection conn = DataBaseHelper.DepperSqlserver())
{
List datas = conn.Query(sql, param).ToList();
return datas;
}
}
public static T Find(string sql, object param)
{
using (IDbConnection conn = DataBaseHelper.DepperSqlserver())
{
T datas = conn.Query(sql, param).SingleOrDefault();
return datas;
}
}
public static int Count(string sql, object param)
{
using (IDbConnection conn = DataBaseHelper.DepperSqlserver())
{
List datas = conn.Query(sql, param).ToList();
return datas.ToArray().Length;
}
}
public static int Update(string sql, object param)
{
using (IDbConnection conn = DataBaseHelper.DepperSqlserver())
{
return conn.Execute(sql, param);
}
}
public static int Update(string firstUpdateSql,object firstParam, string secondUpdateSql,object secondParam)
{
using (IDbConnection conn = DataBaseHelper.DepperSqlserver())
{
IDbTransaction transaction = conn.BeginTransaction();
int firstResult = conn.Execute(firstUpdateSql, firstParam, transaction);
int secondResult = conn.Execute(secondUpdateSql, secondParam, transaction);
transaction.Commit();
return firstResult + secondResult;
}
}
public static int Add(string sql, object param)
{
using (IDbConnection conn = DataBaseHelper.DepperSqlserver())
{
return conn.Execute(sql, param);
}
}
public static int Execute(string sql, object param)
{
using (IDbConnection conn = DataBaseHelper.DepperSqlserver())
{
return conn.Execute(sql, param);
}
}
}
订单实体类
public class OrgOrder
{
///
/// 主键
///
private long _orgorderid;
public long OrgOrderId
{
get { return _orgorderid; }
set { _orgorderid = value; }
}
///
/// 总价
///
private decimal _payamount;
public decimal PayAmount
{
get { return _payamount; }
set { _payamount = value; }
}
///
/// 订单编号
///
private string _ordercode;
public string OrderCode
{
get { return _ordercode; }
set { _ordercode = value; }
}
///
/// BuyerId
///
private long _buyerid;
public long BuyerId
{
get { return _buyerid; }
set { _buyerid = value; }
}
///
/// OrderState
///
private int _orderstate;
public int OrderState
{
get { return _orderstate; }
set { _orderstate = value; }
}
///
/// 是否删除
///
private int _isdel;
public int IsDel
{
get { return _isdel; }
set { _isdel = value; }
}
}
4) . 编写要执行的作业即job
public class OrderJob : IJob
{
//private readonly ILog _logger = LogManager.GetLogger(typeof(OrgOrder));
ILog _logger = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
public void Execute(IJobExecutionContext context)
{
try
{
_logger.Info("*Info***************清理机构订单 开始执行****************");
var orgOrderList = GetOrgOrder();
if (orgOrderList != null && orgOrderList.Any())
{
var orderCodes = orgOrderList.Select(x => x.OrderCode).ToList();
string orderCodeCollection = string.Join(",", orderCodes);
_logger.InfoFormat("*Info**************需要清理的机构订单{0}个****************", orgOrderList.Count);
_logger.InfoFormat("*Info**************清理的订单对于编号是{0}****************", orderCodeCollection);
ClearOrgOrder(orgOrderList);
}
else
{
_logger.Info("*Info***************没有要清理的机构订单****************");
}
_logger.Info("*Info***************清理机构订单 执行结束****************");
}
catch (Exception ex)
{
_logger.Error("***ERROR***清理机构订单***" + ex.Message + ex.StackTrace);
}
}
///
/// 获取未支付的过期订单
///
///
public List GetOrgOrder()
{
return DapperHelper.Select(@"SELECT
BuyerId ,
Description ,
IsDel ,
OrderCode ,
OrderState ,
OrgOrderId ,
PayAmount ,
PayType
FROM dbo.OrgOrder
WHERE IsDel=0 AND (OrderState is null or OrderState=@OrderState) AND CreateTime < @CreateTime", new { OrderState = 1, CreateTime = DateTime.Now.AddMinutes(0 - ExpireConfig.OrgOrderExpire) });
}
///
/// 清理体测,更新库存
///
public void ClearOrgOrder(List orgOrderList)
{
int totalResult = DapperHelper.Update(@"UPDATE
orgorder
SET
OrderState = 1
WHERE
ordercode = @OrderCode;", orgOrderList,
@"UPDATE
dbo.SignDetail
SET
IsDel = 1
WHERE
IsDel = 0
AND Studentid = @BuyerId
AND Courseid IN ( SELECT
ProductId
FROM
dbo.OrgOrderDetail
WHERE
OrderCode = @OrderCode )", orgOrderList);
_logger.InfoFormat("*Success***************成功清理机构订单+恢复库存一共{0}个****************", totalResult);
}
}
5) . 添加TopShlf插件配置,将程序转化成window服务
配置quartz.config
# You can configure your scheduler in either configuration section
# or in quartz properties file
# Configuration section has precedence
quartz.scheduler.instanceName = QuartzTest
# configure thread pool info
quartz.threadPool.type = Quartz.Simpl.SimpleThreadPool, Quartz
quartz.threadPool.threadCount = 10
quartz.threadPool.threadPriority = Normal
# job initialization plugin handles our xml reading, without it defaults are used
quartz.plugin.xml.type = Quartz.Plugin.Xml.XMLSchedulingDataProcessorPlugin, Quartz
quartz.plugin.xml.fileNames = ~/quartz_jobs.xml
# export this server to remoting context
#quartz.scheduler.exporter.type = Quartz.Simpl.RemotingSchedulerExporter, Quartz
#quartz.scheduler.exporter.port = 555
#quartz.scheduler.exporter.bindName = QuartzScheduler
#quartz.scheduler.exporter.channelType = tcp
#quartz.scheduler.exporter.channelName = httpQuartz
配置quartz_jobs.xml 该文件主要配置job调度的时间规则(规定job每隔半个小时调度一边)
<job-scheduling-data xmlns="http://quartznet.sourceforge.net/JobSchedulingData" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.0">
<processing-directives>
<overwrite-existing-data>trueoverwrite-existing-data>
processing-directives>
<schedule>
<job>
<name>OrderJobname>
<group>OrderJobGroupgroup>
<description>未支付的机构订单清理description>
<job-type>Fantasy.SmartTraining.Job.Code.OrderJob,Fantasy.SmartTraining.Jobjob-type>
<durable>truedurable>
<recover>falserecover>
job>
<trigger>
<cron>
<name>OrderJobTriggername>
<group>OrderJobGroupgroup>
<job-name>OrderJobjob-name>
<job-group>OrderJobGroupjob-group>
<start-time>2017-05-25T00:00:00+01:00start-time>
<cron-expression>0 0/30 * * * ?cron-expression>
cron>
trigger>
schedule>
job-scheduling-data>
配置log4net.config
<configuration>
<configSections>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net"/>
configSections>
<log4net>
<appender name="RollingLogFileAppender" type="log4net.Appender.RollingFileAppender">
<param name= "File" value= "C:\App_Log\servicelog\"/>
<param name= "AppendToFile" value= "true"/>
<param name= "MaxSizeRollBackups" value= "10"/>
<param name= "StaticLogFileName" value= "false"/>
<param name= "DatePattern" value= "yyyy-MM-dd".read.log""/>
<param name= "RollingStyle" value= "Date"/>
<layout type="log4net.Layout.PatternLayout">
<param name="ConversionPattern" value="%d [%t] %-5p %c - %m%n %loggername" />
layout>
appender>
<appender name="ColoredConsoleAppender" type="log4net.Appender.ColoredConsoleAppender">
<mapping>
<level value="ERROR" />
<foreColor value="Red, HighIntensity" />
mapping>
<mapping>
<level value="Info" />
<foreColor value="Green" />
mapping>
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%n%date{HH:mm:ss,fff} [%-5level] %m" />
layout>
<filter type="log4net.Filter.LevelRangeFilter">
<param name="LevelMin" value="Info" />
<param name="LevelMax" value="Fatal" />
filter>
appender>
<root>
<level value="all" />
<appender-ref ref="ColoredConsoleAppender"/>
<appender-ref ref="RollingLogFileAppender"/>
root>
log4net>
configuration>
说明:这三个文件,分别选中→右键属性→复制到输入目录设为:始终复制
6) . 添加ServiceRunner
public sealed class ServiceRunner : ServiceControl, ServiceSuspend
{
private readonly IScheduler scheduler;
public ServiceRunner()
{
scheduler = StdSchedulerFactory.GetDefaultScheduler();
}
public bool Start(HostControl hostControl)
{
scheduler.Start();
return true;
}
public bool Stop(HostControl hostControl)
{
scheduler.Shutdown(false);
return true;
}
public bool Continue(HostControl hostControl)
{
scheduler.ResumeAll();
return true;
}
public bool Pause(HostControl hostControl)
{
scheduler.PauseAll();
return true;
}
}
7) .程序入口
static void Main(string[] args)
{
//ISchedulerFactory ischedulerFactory = new StdSchedulerFactory();
//IScheduler scheduler = ischedulerFactory.GetScheduler();
//IJobDetail job = JobBuilder.Create().Build();
//ISimpleTrigger trigger = (ISimpleTrigger)TriggerBuilder.Create().StartAt(DateTime.Now).EndAt(DateTime.Now.AddHours(2)).WithSimpleSchedule(x => x.WithIntervalInSeconds(1).WithRepeatCount(10)).Build();
4.加入作业调度池中
//scheduler.ScheduleJob(job, trigger);
5.开始运行
//scheduler.Start();
//Console.ReadKey();
log4net.Config.XmlConfigurator.ConfigureAndWatch(new FileInfo(AppDomain.CurrentDomain.BaseDirectory + "JobConfig/log4net.config"));
HostFactory.Run(x =>
{
x.UseLog4Net();
x.Service();
x.SetDescription("SmartTrainJob服务描述");
x.SetDisplayName("SmartTrainJob服务显示名称");
x.SetServiceName("SmartTrainJob服务名称");
x.EnablePauseAndContinue();
});
}
9). 安装成服务
开始安装(E:\TrainForTest\Job\Job.exe存在与当前程序的Debug文件夹下)
E:\TrainForTest\Job\Fantasy.SmartTraining.Job.exe install
启动
E:\TrainForTest\Job\Job.exe start
卸载
E:\TrainForTest\Job\Job.exe uninstall
服务安装效果如下:
3. Quartz框架中重要知识点
一个小小例子走过来对Quartz插件是否有一点点了解;接下来让我们细细品味该Quartz插件内在美吧
看看下图:
图中scheduler就是任务调度器
trigger就是触发器,用于定义任务调度时间规则
job就是作业即任务,即被调度的任务
misfire 错过的,指本来应该被执行但实际没有被执行的任务调度
Job:是一个接口,只有一个方法void execute(JobExecutionContext context),开发者实现该接口定义运行任务,JobExecutionContext类提供了调度上下文的各种信息。Job运行时的信息保存在JobDataMap实例中;
JobDetail:Quartz在每次执行Job时,都重新创建一个Job实例,所以它不直接接受一个Job的实例,相反它接收一个Job实现类,以便运行时通过newInstance()的反射机制实例化Job。因此需要通过一个类来描述Job的实现类及其它相关的静态信息,如Job名字、描述、关联监听器等信息,JobDetail承担了这一角色。
Trigger:是一个类,描述触发Job执行的时间触发规则。主要有SimpleTrigger和CronTrigger这两个子类。当仅需触发一次或者以固定时间间隔周期执行,SimpleTrigger是最适合的选择;而CronTrigger则可以通过Cron表达式定义出各种复杂时间规则的调度方案:如每早晨9:00执行,周一、周三、周五下午5:00执行等;
Calendar:quartz.Calendar它是一些日历特定时间点的集合,一个Trigger可以和多个Calendar关联,以便排除或包含某些时间点。假设,我们安排每周星期一早上10:00执行任务,但是如果碰到法定的节日,任务则不执行,这时就需要在Trigger触发机制的基础上使用Calendar进行定点排除。
Scheduler:代表一个Quartz的独立运行容器,Trigger和JobDetail可以注册到Scheduler中,两者在Scheduler中拥有各自的组及名称,组及名称是Scheduler查找定位容器中某一对象的依据,Trigger的组及名称必须唯一,JobDetail的组和名称也必须唯一(但可以和Trigger的组和名称相同,因为它们是不同类型的)。Scheduler定义了多个接口方法,允许外部通过组及名称访问和控制容器中Trigger和JobDetail。
Scheduler可以将Trigger绑定到某一JobDetail中,这样当Trigger触发时,对应的Job就被执行。一个Job可以对应多个Trigger,但一个Trigger只能对应一个Job。可以通过SchedulerFactory创建一个Scheduler实例。Scheduler拥有一个SchedulerContext,它类似于ServletContext,保存着Scheduler上下文信息,Job和Trigger都可以访问SchedulerContext内的信息。SchedulerContext内部通过一个Map,以键值对的方式维护这些上下文数据,SchedulerContext为保存和获取数据提供了多个put()和getXxx()的方法。可以通过Scheduler# getContext()获取对应的SchedulerContext实例;
ThreadPool:Scheduler使用一个线程池作为任务运行的基础设施,任务通过共享线程池中的线程提高运行效率。
4.Quartz框架学习
Quartz调度器
Quartz框架的核心是调度器。调度器负责管理Quartz应用运行时环境。调度器不是靠自己做所有的工作,而是依赖框架内一些非常重要的部件。Quartz不仅仅是线程和线程管理。为确保可伸缩性,Quartz采用了基于多线程的架构。
启动时,框架初始化一套worker线程,这套线程被调度器用来执行预定的作业。这就是Quartz怎样能并发运行多个作业的原理。Quartz依赖一套松耦合的线程池管理部件来管理线程环境。本文中,我们会多次提到线程池管理,但Quartz里面的每个对象是可配置的或者是可定制的。所以,例如,如果你想要插进自己线程池管理设施,我猜你一定能!
作业
用Quartz的行话讲,作业是一个执行任务的简单C#类。任务可以是任何C#代码。只需你实现quartz.Job接口并且在出现严重错误情况下抛出JobExecutionException异常即可。
Job接口包含唯一的一个方法execute(),作业从这里开始执行。一旦实现了Job接口和execute()方法,当Quartz确定该是作业运行的时候,它将调用你的作业。Execute()方法内就完全是你要做的事情。
作业管理和存储
作业一旦被调度,调度器需要记住并且跟踪作业和它们的执行次数。如果你的作业是30分钟后或每30秒调用,这不是很有用。事实上,作业执行需要非常准确和即时调用在被调度作业上的execute()方法。Quartz通过一个称之为作业存储(JobStore)的概念来做作业存储和管理。
有效作业存储
Quartz提供两种基本作业存储类型。第一种类型叫做RAMJobStore,它利用通常的内存来持久化调度程序信息。这种作业存储类型最容易配置、构造和运行。对许多应用来说,这种作业存储已经足够了。
然而,因为调度程序信息是存储在被分配给内存里面,所以,当应用程序停止运行时,所有调度信息将被丢失。如果你需要在重新启动之间持久化调度信息,则将需要第二种类型的作业存储。
第二种类型的作业存储实际上提供两种不同的实现,但两种实现一般都称为JDBC作业存储。两种JDBC作业存储都需要JDBC驱动程序和后台数据库来持久化调度程序信息。这两种类型的不同在于你是否想要控制数据库事务或这释放控制给应用服务器例如BEA’s WebLogic或Jboss。(这类似于J2EE领域中,Bean管理的事务和和容器管理事务之间的区别)这两种JDBC作业存储是:
· JobStoreTX:当你想要控制事务或工作在非应用服务器环境中是使用
· JobStoreCMT:当你工作在应用服务器环境中和想要容器控制事务时使用。
JDBC作业存储为需要调度程序维护调度信息的用户而设计。
作业和触发器
Quartz设计者做了一个设计选择来从调度分离开作业。Quartz中的触发器用来告诉调度程序作业什么时候触发。框架提供了一把触发器类型,但两个最常用的是SimpleTrigger和CronTrigger。SimpleTrigger为需要简单打火调度而设计。
典型地,如果你需要在给定的时间和重复次数或者两次打火之间等待的秒数打火一个作业,那么SimpleTrigger适合你。另一方面,如果你有许多复杂的作业调度,那么或许需要CronTrigger。
CronTrigger是基于Calendar-like调度的。当你需要在除星期六和星期天外的每天上午10点半执行作业时,那么应该使用CronTrigger。正如它的名字所暗示的那样,CronTrigger是基于Unix克隆表达式的。
作为一个例子,下面的Quartz克隆表达式将在星期一到星期五的每天上午10点15分执行一个作业。
0 15 10 ? * MON-FRI
下面的表达式
0 15 10 ? * 6L 2002-2005
将在2002年到2005年的每个月的最后一个星期五上午10点15分执行作业。你不可能用SimpleTrigger来做这些事情。你可以用两者之中的任何一个,但哪个跟合适则取决于你的调度需要。
如果执行过于复杂的时间规则就要使用CronTrigger,而CronTrigger则是基于Calendar-like来调度,想学习Calendar-like语法可以参考quartz.net 时间表达式—– Cron表达式详解此人写的还是蛮生动
错过触发(misfire)
trigger还有一个重要的属性misfire;如果scheduler关闭了,或者Quartz线程池中没有可用的线程来执行job,此时持久性的trigger就会错过(miss)其触发时间,即错过触发(misfire)。不同类型的trigger,有不同的misfire机制。它们默认都使用“智能机制(smart policy)”,即根据trigger的类型和配置动态调整行为。当scheduler启动的时候,查询所有错过触发(misfire)的持久性trigger。然后根据它们各自的misfire机制更新trigger的信息。当你在项目中使用Quartz时,你应该对各种类型的trigger的misfire机制都比较熟悉,这些misfire机制在Doc中有说明。
调度一个作业
让我们通过看一个例子来进入实际讨论。现假定你管理一个部门,无论何时候客户在它的FTP服务器上存储一个文件,都得用电子邮件通知它。我们的作业将用FTP登陆到远程服务器并下载所有找到的文件。
然后,它将发送一封含有找到和下载的文件数量的电子邮件。这个作业很容易就帮助人们整天从手工执行这个任务中解脱出来,甚至连晚上都无须考虑。我们可以设置作业循环不断地每60秒检查一次,而且工作在7×24模式下。这就是Quartz框架完全的用途。
首先创建一个Job类,将执行FTP和Email逻辑。下例展示了Quartz的Job类,它实现了quartz.Job接口。
用调度器调用作业
首先创建一个作业,但为使作业能被调度器调用,你得向调度程序说明你的作业的调用时间和频率。这个事情由与作业相关的触发器来完成。因为我们仅仅对大约每60秒循环调用作业感兴趣,所以打算使用SimpleTrigger。
作业和触发器通过Quartz调度器接口而被调度。我们需要从调度器工厂类取得一个调度器的实例。最容易的办法是调用StdSchedulerFactory这个类上的静态方法getDefaultScheduler()。
使用Quartz框架,你需要调用start()方法来启动调度器。例3的代码遵循了大多数Quartz应用的一般模式:创建一个或多个作业,创建和设置触发器,用调度器调度作业和触发器,启动调度器。
编程调度同声明性调度
我们通过编程的方法调度我们的ScanFTPSiteJob作业。就是说,我们用C#代码来设置作业和触发器。Quartz框架也支持在xml文件里面申明性的设置作业调度。申明性方法允许我们更快速地修改哪个作业什么时候被执行。
Quartz框架有一个插件,这个插件负责读取xml配置文件。xml配置文件包含了关于启动Quartz应用的作业和触发器信息。所有xml文件中的作业连同相关的触发器都被加进调度器。你仍然需要编写作业类,但配置那些作业类的调度器则非常动态化。你可以将xml文件中的元素跟例3代码作个比较,它们从概念上来看是相同的。使用申明性方法的好处是维护变得极其简单,只需改变xml配置文件和重新启动Quartz应用即可。无须修改代码,无须重新编译,无须重新部署。
有状态和无状态作业
作业到是无状态的。这意味着在两次作业执行之间,不会去维护作业执行时JobDataMap的状态改变。如果你需要能增、删,改JobDataMap的值,而且能让作业在下次执行时能看到这个状态改变,则需要用Quartz有状态作业。
Quartz有状态作业实现了quartz.StatefulJob接口。
无状态和有状态作业的关键不同是有状态作业在每次执行时只有一个实例。大多数情况下,有状态的作业不回带来大的问题。然而,如果你有一个需要频繁执行的作业或者需要很长时间才能完成的作业,那么有状态作业可能给你带来伸缩性问题。
监听器和插件
每个人都喜欢监听和插件。今天,几乎下载任何开源框架,你必定会发现支持这两个概念。监听是你创建的C#类,当关键事件发生时会收到框架的回调。例如,当一个作业被调度、没有调度或触发器终止和不再打火时,这些都可以通过设置来来通知你的监听器。Quartz框架包含了调度器监听、作业和触发器监听。你可以配置作业和触发器监听为全局监听或者是特定于作业和触发器的监听。
一旦你的一个具体监听被调用,你就能使用这个技术来做一些你想要在监听类里面做的事情。例如,你如果想要在每次作业完成时发送一个电子邮件,你可以将这个逻辑写进作业里面,也可以JobListener里面。写进JobListener的方式强制使用松耦合有利于设计上做到更好。
Quartz插件是一个新的功能特性,无须修改Quartz源码便可被创建和添加进Quartz框架。他为想要扩展Quartz框架又没有时间提交改变给Quartz开发团队和等待新版本的开发人员而设计。如果你熟悉Struts插件的话,那么完全可以理解Quartz插件的使用。
与其Quartz提供一个不能满足你需要的有限扩展点,还不如通过使用插件来拥有可修整的扩展点。
集群Quartz应用
Quartz应用能被集群,是水平集群还是垂直集群取决于你自己的需要。集群提供以下好处:
· 伸缩性
· 高可用性
· 负载均衡
目前,Quartz只能借助关系数据库和JDBC作业存储支持集群。将来的版本这个制约将消失并且用RAMJobStore集群将是可能的而且将不需要数据库的支持
感谢如下文章的作者,本文也是参考如下文章来做的
Quartz.net开源作业调度框架使用详解 如果您的项目需要多个作业调度同时更加需要实时把控,可以参考上面仁兄的方式来做个页面实时把控日程
quartz详解2:quartz由浅入深
根据该文几个步骤就能搭建好一个作业调度