什么是Quartz?
什么是Quartz
Quartz是一个开源的作业调度框架,Quartz根据用户设定的时间规则来执行作业,使用场景:在平时的工作中,估计大多数都做过轮询调度的任务,比如定时轮询数据库同步,定时邮件通知、定时关闭网上商城的用户还未支付的订单等等。
Quartz运用场景
作业场景:公司商城平台中客户下了订单但是超过半个小时未支付就要取消该订单
1) . 数据库中订单主表主要字段如下:
数据库订单表结构
2) .下载对应插件
Quartz.net插件 —该插件是作业调度的核心框架
Log4net插件 —-该插件是用来记录用户日志
Topshlf插件 —-主要是用来将作业调度程序转化成window服务
Dapper插件 —-用来访问数据的插件(到数据库查询订单数据)
3) .访问数据库帮助类
config中配置数据库连接地址以及订单过期时间配置
<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<T> Select<T>(string sql, object param)
{
using (IDbConnection conn = DataBaseHelper.DepperSqlserver())
{
List<T> datas = conn.Query<T>(sql, param).ToList<T>();
return datas;
}
}
public static T Find<T>(string sql, object param)
{
using (IDbConnection conn = DataBaseHelper.DepperSqlserver())
{
T datas = conn.Query<T>(sql, param).SingleOrDefault<T>();
return datas;
}
}
public static int Count(string sql, object param)
{
using (IDbConnection conn = DataBaseHelper.DepperSqlserver())
{
List<dynamic> 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
{
/// <summary>
/// 主键
/// </summary>
private long _orgorderid;
public long OrgOrderId
{
get { return _orgorderid; }
set { _orgorderid = value; }
}
/// <summary>
/// 总价
/// </summary>
private decimal _payamount;
public decimal PayAmount
{
get { return _payamount; }
set { _payamount = value; }
}
/// <summary>
/// 订单编号
/// </summary>
private string _ordercode;
public string OrderCode
{
get { return _ordercode; }
set { _ordercode = value; }
}
/// <summary>
/// BuyerId
/// </summary>
private long _buyerid;
public long BuyerId
{
get { return _buyerid; }
set { _buyerid = value; }
}
/// <summary>
/// OrderState
/// </summary>
private int _orderstate;
public int OrderState
{
get { return _orderstate; }
set { _orderstate = value; }
}
/// <summary>
/// 是否删除
/// </summary>
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);
}
}
/// <summary>
/// 获取未支付的过期订单
/// </summary>
/// <returns></returns>
public List<OrgOrder> GetOrgOrder()
{
return DapperHelper.Select<OrgOrder>(@"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) });
}
/// <summary>
/// 清理体测,更新库存
/// </summary>
public void ClearOrgOrder(List<OrgOrder> 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 <quartz> 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每隔半个小时调度一边)
<?xml version="1.0" encoding="UTF-8"?>
<!-- This file contains job definitions in schema version 2.0 format -->
<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>true</overwrite-existing-data>
</processing-directives>
<schedule>
<!--机构未支付订单清理-->
<job>
<name>OrderJob</name>
<group>OrderJobGroup</group>
<description>未支付的机构订单清理</description>
<job-type>Fantasy.SmartTraining.Job.Code.OrderJob,Fantasy.SmartTraining.Job</job-type>
<durable>true</durable>
<recover>false</recover>
</job>
<trigger>
<cron>
<name>OrderJobTrigger</name>
<group>OrderJobGroup</group>
<job-name>OrderJob</job-name>
<job-group>OrderJobGroup</job-group>
<start-time>2017-05-25T00:00:00+01:00</start-time>
<cron-expression>0 0/30 * * * ?</cron-expression>
</cron>
</trigger>
</schedule>
</job-scheduling-data>
配置log4net.config
<?xml version="1.0" encoding="utf-8" ?>
<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"/>
<!--log保留天数-->
<param name= "MaxSizeRollBackups" value= "10"/>
<!--日志文件名是否是固定不变的-->
<param name= "StaticLogFileName" value= "false"/>
<!--日志文件名格式为:2008-08-31.log-->
<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>
<!--(高) OFF > FATAL > ERROR > WARN > INFO > DEBUG > ALL (低) -->
<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<OrderJob>().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<ServiceRunner>();
x.SetDescription("SmartTrainJob服务描述");
x.SetDisplayName("SmartTrainJob服务显示名称");
x.SetServiceName("SmartTrainJob服务名称");
x.EnablePauseAndContinue();
});
}
8) . 运行效果
这里写图片描述
生成日志:
这里写图片描述
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语