在上一篇 Quartz.NET 实现作业定时调度详解 http://blog.rdiframework.net/article/220 , 我们通过实例代码详细讲解与演示了基于 Quartz.NET 开发的详细方法. 本篇我们主要讲述基于 RDIFramework.NET 框架整合 Quartz.NET, 以实现任务调度, 并对任务持久化操作的全过程. 本文主要通过以下几个方面讲解:
1, 任务调度概述
2 任务调度管理
2.1,Cron 表达式
2.2, 创建用户过程调度任务
2.3, 创建程序集任务
1, 任务调度概述
任务调度在各种应用中都会存在, 在业务系统中我们为了调度一些自动执行的任务或从队列中消费一些消息, 所以基本上都会涉及到后台服务的开发. 在了解任务调度之前, 我们先了解一下实现任务调度的 Quartz.NET 框架.
Quartz.NET 是一个开源的作业调度框架, 是 OpenSymphony 的 Quartz API 的. NET 移植, 它用 C# 写成, 可用于 winform 和 ASP.NET 应用中. 它提供了巨大的灵活性而不牺牲简单性. 你能够用它来为执行一个作业而创建简单的或复杂的调度. 它有很多特征, 如: 数据库支持, 集群, 插件, 支持 cron-like 表达式等等. 你曾经需要应用执行一个任务吗? 这个任务每天或每周星期二晚上 11:30, 或许仅仅每个月的最后一天执行. 一个自动执行而无须干预的任务在执行过程中如果发生一个严重错误, 应用能够知到其执行失败并尝试重新执行吗? 你和你的团队是用. NET 编程吗? 如果这些问题中任何一个你回答是, 那么你应该使用 Quartz.NET 调度器. Quartz.NET 允许开发人员根据时间间隔 (或天) 来调度作业. 它实现了作业和触发器的多对多关系, 还能把多个作业与不同的触发器关联. 整合了 Quartz.NET 的应用程序可以重用来自不同事件的作业, 还可以为一个事件组合多个作业.
总的来说就是 Quartz.NET 是一个开源的作业调度框架, 非常适合在平时的工作中, 定时轮询数据库同步, 定时邮件通知, 定时处理数据等. Quartz.NET 允许开发人员根据时间间隔 (或天) 来调度作业. 它实现了作业和触发器的多对多关系, 还能把多个作业与不同的触发器关联, 配置灵活方便. 相当于数据库中的 Job,Windows 的计划任务, Unix/Linux 下的 Cron, 但 Quartz 可以把排程控制的更精细, 对任务调度的领域问题进行了高度的抽象, 实现作业的灵活调度.
我们框架的任务调度就是基于 Quartz.NET 框架的整合使用, 并对任务做了持久化的操作.
2, 任务调度管理
"任务列表" 管理模块是放在 "系统配置"->"任务调度" 下的 "任务列表", 任务列表如下图所示. 任务列表主界面左侧显示的是已经创建的任务, 右侧为当前选中任务的执行情况列表. 在左侧的任务列表最左边的操作栏, 可以通过操作按钮对当前任务做删除, 暂停, 启动, 删除任务日志的操作.
在任务列表主界面顶部的工具栏中的按钮可用来创建任务, 创建的任务分为两种类型:
创建用户过程调度任务.
创建程序集任务.
每种类型下的任务又可以分为简单任务与复杂任务. 简单任务类似于定时器每隔特定的间隔时间触发, 复杂任务主要是主要是借助 CronTrigger 表达式来实现类似数据库中的计划任务类型的工作. 使用 CronTrigger 你可以指定诸如 "每个周五中午", 或者 "每个工作日的 9:30" 或者 "从每个周一, 周三, 周五的上午 9:00 到上午 10:00 之间每隔五分钟" 这样日程安排来触发. 甚至像 SimpleTrigger(简单任务)一样, CronTrigger 也有一个 StartTime 以指定日程从什么时候开始, 也有一个(可选的)EndTime 以指定何时日程不再继续.
下面的章节我们分别对这两种任务类型做介绍. 在介绍之前先了解学习一下 Cron 表达式.
2.1,Quartz 的 cron 表达式
Cron 表达式是一个字符串, 字符串以 5 或 6 个空格隔开, 分为 6 或 7 个域, 每一个域代表一个含义, Cron 有如下两种语法格式:
- (1) Seconds Minutes Hours DayofMonth Month DayofWeek Year
- (2)Seconds Minutes Hours DayofMonth Month DayofWeek
一, 结构
corn 从左到右(用空格隔开): 秒 分 小时 月份中的日期 月份 星期中的日期 年份
二, 各字段的含义
注意: 每一个域都使用数字, 但还可以出现如下特殊字符, 它们的含义是:
(1): 表示匹配该域的任意值. 假如在 Minutes 域使用, 即表示每分钟都会触发事件.
(2)?: 只能用在 DayofMonth 和 DayofWeek 两个域. 它也匹配域的任意值, 但实际不会. 因为 DayofMonth 和 DayofWeek 会相互影响. 例如想在每月的 20 日触发调度, 不管 20 日到底是星期几, 则只能使用如下写法: 13 13 15 20 * ?, 其中最后一位只能用?, 而不能使用, 如果使用表示不管星期几都会触发, 实际上并不是这样.
(3)-: 表示范围. 例如在 Minutes 域使用 5-20, 表示从 5 分到 20 分钟每分钟触发一次
(4)/: 表示起始时间开始触发, 然后每隔固定时间触发一次. 例如在 Minutes 域使用 5/20, 则意味着 5 分钟触发一次, 而 25,45 等分别触发一次.
(5),: 表示列出枚举值. 例如: 在 Minutes 域使用 5,20, 则意味着在 5 和 20 分每分钟触发一次.
(6)L: 表示最后, 只能出现在 DayofWeek 和 DayofMonth 域. 如果在 DayofWeek 域使用 5L, 意味着在最后的一个星期四触发.
(7)W: 表示有效工作日 (周一到周五), 只能出现在 DayofMonth 域, 系统将在离指定日期的最近的有效工作日触发事件. 例如: 在 DayofMonth 使用 5W, 如果 5 日是星期六, 则将在最近的工作日: 星期五, 即 4 日触发. 如果 5 日是星期天, 则在 6 日(周一) 触发; 如果 5 日在星期一到星期五中的一天, 则就在 5 日触发. 另外一点, W 的最近寻找不会跨过月份 .
(8)LW: 这两个字符可以连用, 表示在某个月最后一个工作日, 即最后一个星期五.
(9)#: 用于确定每个月第几个星期几, 只能出现在 DayofMonth 域. 例如在 4#2, 表示某月的第二个星期三.
三, 常用表达式例子
(1)0 0 2 1 * ? * 表示在每月的 1 日的凌晨 2 点调整任务
(2)0 15 10 ? * MON-FRI 表示周一到周五每天上午 10:15 执行作业
(3)0 15 10 ? 6L 2002-2006 表示 2002-2006 年的每个月的最后一个星期五上午 10:15 执行作
(4)0 0 10,14,16 * * ? 每天上午 10 点, 下午 2 点, 4 点
(5)0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时
(6)0 0 12 ? * WED 表示每个星期三中午 12 点
(7)0 0 12 * * ? 每天中午 12 点触发
(8)0 15 10 ? * * 每天上午 10:15 触发
(9)0 15 10 * * ? 每天上午 10:15 触发
(10)0 15 10 * * ? * 每天上午 10:15 触发
(11)0 15 10 * * ? 2005 2005 年的每天上午 10:15 触发
(12)0 * 14 * * ? 在每天下午 2 点到下午 2:59 期间的每 1 分钟触发
(13)0 0/5 14 * * ? 在每天下午 2 点到下午 2:55 期间的每 5 分钟触发
(14)0 0/5 14,18 * * ? 在每天下午 2 点到 2:55 期间和下午 6 点到 6:55 期间的每 5 分钟触发
(15)0 0-5 14 * * ? 在每天下午 2 点到下午 2:05 期间的每 1 分钟触发
(16)0 10,44 14 ? 3 WED 每年三月的星期三的下午 2:10 和 2:44 触发
(17)0 15 10 ? * MON-FRI 周一至周五的上午 10:15 触发
(18)0 15 10 15 * ? 每月 15 日上午 10:15 触发
(19)0 15 10 L * ? 每月最后一日的上午 10:15 触发
(20)0 15 10 ? * 6L 每月的最后一个星期五上午 10:15 触发
(21)0 15 10 ? * 6L 2002-2005 2002 年至 2005 年的每月的最后一个星期五上午 10:15 触发
(22)0 15 10 ? * 6#3 每月的第三个星期五上午 10:15 触发
注:
(1)有些子表达式能包含一些范围或列表
例如:
子表达式 (天(星期)) 可以为 "MON-FRI","MON,WED,FRI","MON-WED,SAT"
"*" 字符代表所有可能的值,
因此,""在子表达式 (月) 里表示每个月的含义,"" 在子表达式 (天(星期)) 表示星期的每一天
"/" 字符用来指定数值的增量
例如: 在子表达式 (分钟) 里的 "0/15" 表示从第 0 分钟开始, 每 15 分钟
在子表达式 (分钟) 里的 "3/20" 表示从第 3 分钟开始, 每 20 分钟 (它和 "3,23,43") 的含义一样
"?" 字符仅被用于天 (月) 和天 (星期) 两个子表达式, 表示不指定值
当 2 个子表达式其中之一被指定了值以后, 为了避免冲突, 需要将另一个子表达式的值设为 "?"
"L" 字符仅被用于天 (月) 和天 (星期) 两个子表达式, 它是单词 "last" 的缩写
但是它在两个子表达式里的含义是不同的.
在天 (月) 子表达式中,"L" 表示一个月的最后一天
在天 (星期) 自表达式中,"L" 表示一个星期的最后一天, 也就是 SAT
如果在 "L" 前有具体的内容, 它就具有其他的含义了
例如:"6L" 表示这个月的倒数第6天,"FRIL" 表示这个月的最一个星期五
注意: 在使用 "L" 参数时, 不要指定列表或范围, 因为这会导致问题
四, 表达式生成器
有很多的 cron 表达式在线生成器, 这里给大家推荐几款
http://www.pdtools.net/tools/becron.jsp
或者
http://cron.qqe2.com/
2.2, 创建用户过程调度任务
过程调度任务简单的理解就是可以执行 SQL 语句或存储过程等. 创建用户过程调度任务如下图所示.
在创建用户过程调试界面,"过程 SQL" 就是执行的 SQL 语句或存储过程或函数等. 过程参数就是过程 SQL 中的参数列表对应的参数值. 创建的任务默认是简单任务, 如上图我们创建了一个每 1 分钟执行一次的简单过程任务, 其实 "无限次" 选中就表示不限次数, 否则可以指定执行的次数. 要创建复杂任务可以单击 "复杂任务" 选项卡, 如下图所示.
复杂任务中的各时间项的配置就是 Cron 表达式, 每单击一个配置项, 右侧都对该配置项进行了详细的设置说明. 设置好后可以单击 "检查表达式" 来验证 Cron 表达式的正确性, 如下图所示.
单击确认按钮即可成功创建任务. 要删除, 暂停, 重启, 删除任务日志, 只需选中任务后单击当前任务左侧的操作按钮区域对应的操作按钮即可, 如下图所示.
2.3, 创建程序集任务
程序集任务简单的理解就是创建一个自动执行的 C# 方法, 程序集任务与用户过程调度任务类似, 也分简单的任务与复杂的任务, 创建程序集任务如下图所示.
我们在微信公众号开发系列 - 玩转微信开发 - 目录汇总 http://blog.rdiframework.net/article/216 系列文章中对微信开发进行了详细的讲解, 我们知道微信提供的 API 大多都是以微信分配给我们的一个 access_token 为基础, Access Token 相当于打开这些服务的钥匙, 正常情况下会在 7200 秒内失效. 对于 access_tokenr 的详细介绍可参考我们的: 微信公众号开发系列 - 4, 获取接口调用凭证 http://blog.rdiframework.net/article/207
前面的开发我们都是失效后各自应用去重新获取 access_token. 虽然这样操作可行但不是理想的操作方式, 理想的操作方式应该是后台定时自动刷新我们得到的 access_token. 我们可以使用任务调度来实现 access_token 的获取.
在上图中我们创建了一个每 30 分钟自动更新微信公众号开发中的 access_token, 配置项的中程序集名称格式为: 命名空间 + 类, 具体的开发方法可以参考我们任务调度中的 Job 事例. 编写的 job 只需要继承我们的基类: ITaskJob, 并实现以下方法即可.
- ,public string RunJob(ref JobDataMap dataMap, string jobName, string id, string taskName)
- ,public string RunJobBefore(JobEntity jobModel)
- ,public string CloseJob(JobEntity jobModel)
参考代码如下:
- public class WeChatGetTokenJob : ITaskJob
- {
- public string RunJob(ref JobDataMap dataMap, string jobName, string id, string taskName)
- {
- int returnValue = 0;
- List<KeyValuePair<string, object>> parmeters = new List<KeyValuePair<string, object>>
- {
- new KeyValuePair<string, object>(WeixinOfficialAccountTable.FieldDeleteMark, 0)
- };
- var listOfficialAccount = BaseEntity.GetList<WeixinOfficialAccountEntity>(RDIFrameworkService.Instance.WeixinBasicService.GetOfficialAccountDTByValues(parmeters));
- if (listOfficialAccount != null && listOfficialAccount.Count()> 0)
- {
- foreach (WeixinOfficialAccountEntity entity in listOfficialAccount)
- {
- try
- {
- if (entity.Category == (int)WeChatSubscriberEnum.EnterpriseSubscriber)
- {
- if (!string.IsNullOrEmpty(entity.AppId) && !string.IsNullOrEmpty(entity.AppSecret))
- {
- // 方法一: 使用 Senparc.WeiXin SDK 的方法
- entity.AccessToken = Senparc.Weixin.QY.CommonAPIs.CommonApi.GetToken(entity.AppId, entity.AppSecret).access_token;
- entity.ModifiedBy = "job_rdiframework";
- // 方式二, 直接调用微信的接口方法
- //var url = string.Format("https://api.weixin.qq.com/cgi-bin/token?grant_type={0}&appid={1}&secret={2}", "client_credential".AsUrlData(), entity.AppId.AsUrlData(), entity.AppSecret.AsUrlData());
- //AccessTokenResult result = Get.GetJson<AccessTokenResult>(url);
- //entity.AccessToken = result.access_token;
- entity.ModifiedOn = DateTime.Now;
- returnValue += RDIFrameworkService.Instance.WeixinBasicService.UpdateOfficialAccount(entity);
- }
- }
- else
- {
- if (!string.IsNullOrEmpty(entity.AppId) && !string.IsNullOrEmpty(entity.AppSecret))
- {
- // 方法一: 使用 Senparc.WeiXin SDK 的方法
- entity.AccessToken = Senparc.Weixin.MP.CommonAPIs.CommonApi.GetToken(entity.AppId, entity.AppSecret).access_token;
- entity.ModifiedBy = "job_rdiframework";
- // 方式二, 直接调用微信的接口方法
- //var url = string.Format("https://api.weixin.qq.com/cgi-bin/token?grant_type={0}&appid={1}&secret={2}", "client_credential".AsUrlData(), entity.AppId.AsUrlData(), entity.AppSecret.AsUrlData());
- //AccessTokenResult result = Get.GetJson<AccessTokenResult>(url);
- //entity.AccessToken = result.access_token;
- entity.ModifiedOn = DateTime.Now;
- returnValue += RDIFrameworkService.Instance.WeixinBasicService.UpdateOfficialAccount(entity);
- }
- }
- }
- catch (Exception ex)
- {
- LogHelper.WriteException(ex);
- }
- }
- }
- if (returnValue> 0)
- {
- TaskJob.UpdateState(jobName, 1, "成功");
- }
- return "批量更新 Access_Token!";
- }
- public string RunJobBefore(JobEntity jobModel)
- {
- Log.Write("RunJobBefor", jobModel.taskName,"运行");
- List<KeyValuePair<string, object>> parmeters = new List<KeyValuePair<string, object>>
- {
- new KeyValuePair<string, object>(WeixinOfficialAccountTable.FieldDeleteMark, 0)
- };
- var listOfficialAccount = BaseEntity.GetList<WeixinOfficialAccountEntity>(RDIFrameworkService.Instance.WeixinBasicService.GetOfficialAccountDTByValues(parmeters));
- if (listOfficialAccount == null || listOfficialAccount.Count() <= 0)
- {
- return "没有符合获取 Access_Token 的数据!";
- }
- return null;
- }
- public string CloseJob(JobEntity jobModel)
- {
- Log.Write("CloseJob", jobModel.taskName,"关闭");
- TaskJob.UpdateState(jobModel.id, 3, "挂起");
- return "关闭获取 Access_Token 任务";
- }
- }
来源: https://www.cnblogs.com/huyong/p/11180046.html