快速掌握 Quartz 定时调度使用
一、Timer 和 Quartz 简介
我们在实际开发工作中,经常会遇到需要定期或周期性处理一些业务数据的场景,比如定期清理垃圾历史数据,周期性生成和更新缓存数据等等。早期使用 C# 开发代码,绝大部分情况下,我们都会采用 System.Timers.Timer 的对象实例,通过设置它的 Interval 属性确定执行的周期时间,然后在它的 Elapsed 事件中编写业务逻辑代码,如下面代码所示:
static void Main(string[] args)
{
System.Timers.Timer tt = new System.Timers.Timer();
// 每 5 秒执行一次
tt.Interval = 5000;
// 给 timer 指定具体的事件方法
tt.Elapsed += Tt_Elapsed;
// 启动并开始执行
tt.Enabled = true;
// 阻塞控制台主进程,防止其自动关闭控制台
Console.ReadKey();
}
// 定期需要执行的事件方法
private static void Tt_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
// ... 具体的业务逻辑实现代码
}
以上代码实现起来非常简单,很适合简单的业务场景,但是如果遇到复杂业务场景的话,就会比较麻烦,需要自己编写大量的代码去实现,这样就很难保障代码的可读性、维护性、健壮性,增加我们的实现成本。在当今互联网这么发达的年代,像这种非常普遍的应用场景,肯定已经有很多高度封装的组件,能够简化并解决我们在复杂场景所遇到的问题。今天我就向大家推荐一款当前比较流行的定时调度组件:Quartz 。它的 .NET 版本的官网地址是:https://www.quartz-scheduler.net
Quartz 定时调度组件相比于 System.Timers.Timer 具有以下优点:
- 可以灵活指定任务执行的时间周期,执行次数,甚至是精准的执行时刻点(比如每天的零点执行)
- 可以通过任务自带的生命周期事件,实现对任务执行前后的监控,或者增加其它相关的公共的业务逻辑代码
- 可以很轻松的通过配置控制任务是否并发执行。比如我们经常会遇到这样的场景:任务在上一个周期时间结束后还没执行完,下一个周期时间就到来了
- 可以保障任务执行的持续性,不会因为任务中出现异常错误,导致整体任务调度停止。本次任务出现异常,只会导致本次任务崩溃终止,不影响下一个周期的到来并继续执行任务
也许还有更多的优点有待发现。我们在软件开发过程中,尽量多学习和使用一些第三方免费成熟的开源组件,避免重复造轮子,这样就可以站在巨人的肩上,提高开发效率,节约公司成本。说了那么多也不是很直观,下面我们还是通过代码来体会吧。
二、Quartz 快速示例
我们以 .NET5 创建的控制台程序为例,要想实现一个简单的 Quartz 程序,分以下几个步骤:
1 使用 NuGet 安装 Quartz 程序包
打开 NuGet 可视化管理器,搜索 Quartz 并安装程序包,如下图所示:
2 创建并编写要执行的任务 Class 类
我们创建一个 TestJob 类,实现 Quartz.IJob 接口,代码如下:
using Quartz;
using System;
using System.Threading.Tasks;
public class TestJob : IJob
{
public Task Execute(IJobExecutionContext context)
{
return Task.Run(() =>
{
Console.WriteLine($"111打印信息测试..., {DateTime.Now}");
});
}
}
IJob 接口只定义了一个方法 Execute,该方法就是编写业务逻辑代码的地方,这里打印出一段文本和时间。从代码的实现可以看出:每次执行任务都是异步执行的,也就是说程序会创建一个新的线程去执行任务。
3 在 Main 方法中编写调用代码
using Quartz;
using Quartz.Impl;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace QuartZdemo
{
class Program
{
static void Main(string[] args)
{
//基于 TestJob 创建一个任务的实例
IJobDetail jobDetail = JobBuilder.Create<TestJob>().Build();
//定义并创建任务执行的时间策略实例
//这里使用简单策略:每 3 秒执行一次,重复执行 3 次
ITrigger trigger = TriggerBuilder.Create()
.WithSimpleSchedule(s => s.WithIntervalInSeconds(3).WithRepeatCount(3))
.Build();
//创建调度实例
StdSchedulerFactory factory = new StdSchedulerFactory();
IScheduler scheduler = factory.GetScheduler().GetAwaiter().GetResult();
//将刚才创建的 任务实例 和 时间策略实例 相结合,添加到调度实例中
scheduler.ScheduleJob(jobDetail, trigger).GetAwaiter().GetResult();
//启动并运行调度实例
scheduler.Start().GetAwaiter().GetResult();
Console.ReadKey();
}
}
}
执行后的效果如下:
我们定义的时间策略是:每 3 秒执行一次,重复执行 3 次。
从实际执行的效果可以看出:程序启动后就会立即执行一次,然后再执行 3 次,一共执行 4 次。
如果你想要在程序启动后过 2 秒后再执行,每 3 秒执行一次,重复执行 3 次,一共执行 4 次,可以使用 StartAt 方法。
只需要修改时间调度策略的代码即可,代码如下:
ITrigger trigger = TriggerBuilder.Create()
.StartAt(new DateTimeOffset(DateTime.Now.AddSeconds(2)))
.WithSimpleSchedule(s => s.WithIntervalInSeconds(3).WithRepeatCount(3))
.Build();
如果想要永远重复执行的话,可以使用 RepeatForever() ,代码如下:
ITrigger trigger = TriggerBuilder.Create()
.StartAt(new DateTimeOffset(DateTime.Now.AddSeconds(2)))
.WithSimpleSchedule(s => s.WithIntervalInSeconds(3).RepeatForever())
.Build();
我们修改任务代码,让任务执行过程中抛出异常,代码如下:
using Quartz;
using System;
using System.Threading.Tasks;
public class TestJob : IJob
{
public Task Execute(IJobExecutionContext context)
{
return Task.Run(() =>
{
Console.WriteLine($"111打印信息测试..., {DateTime.Now}");
Console.WriteLine("此程序要抛出异常");
//在这里添加代码,抛出异常
throw new Exception("程序异常");
//下面这句代码,因为上面抛出异常,无法被执行
Console.WriteLine("抛出异常了");
});
}
}
运行程序,执行效果如下:
从执行的效果可以看出:虽然任务中的代码抛出异常,但是整体程序仍然持续运行,按照之前设置的时间策略,仍然是程序启动时执行一次,然后每隔 3 秒重复执行 3 次,一共执行 4 次。每次执行任务代码时,抛出异常之前的代码可以被执行,抛出异常之后的代码不会被执行。
三、Quartz 更多知识点介绍
上面的代码只是一个快速的演示 demo,下面我们进行更多的知识点展示:
1 强大的 Cron 时间调度策略
我们在上面的 demo 示例中,使用的是简单调度策略,其实 Quartz 可以使用 Cron 表达式定义时间调度策略。
Cron 表达式最初是在 linux 系统中使用,目前已经被广泛使用到其它应用中,功能非常强大。
网上有很多在线生成 Cron 表达式的网站,比如:https://cron.qqe2.com
假如我们想在每天凌晨 1 点和下午 13 点执行任务,可以使用以下代码定义时间策略:
ITrigger trigger = TriggerBuilder.Create()
.WithCronSchedule("0 0 1,13 * * ? ").Build();
如果大家感兴趣的话,可以详细学习和研究一下 Cron 表达式。
我个人认为:只需要整体了解即可,没必要死记硬背具体的细节,
因为绝大部分情况下,我们都是在用到的时候,直接到网上在线生成 Cron 表达式。
2 控制任务的并发执行
Quartz 每次执行任务时,都是新创建一个线程来执行。
当我们设置的时间周期策略比较短,就会造成上一个周期的任务还没有执行完,下一个周期就到来了,
此时上一个周期的任务和下一个周期的任务会同时执行,因为每个线程之间都是独立的执行,互相不影响。
绝大部分情况下,我们不希望这种情况发生,因为有可能会导致我们的业务数据混乱,
我们只需要在任务类上面增加 [DisallowConcurrentExecution] 标记即可,
这样就能保证上一个周期的任务还没执行完之前,下一个周期到来了就会被忽略,代码如下所示:
[DisallowConcurrentExecution]
public class TestJob : IJob
{
public Task Execute(IJobExecutionContext context)
{
//业务逻辑代码实现....
}
}
3 向任务传递参数信息
我们在任何地方,操作任务对象的实例,向任务传递参数信息,如下代码所示:
static void Main(string[] args)
{
IJobDetail jobDetail = JobBuilder.Create<TestJob>().Build();
//向任务传递两个参数
jobDetail.JobDataMap["parm1"] = "我是参数1";
jobDetail.JobDataMap["parm2"] = "我是参数2";
ITrigger trigger = TriggerBuilder.Create()
.WithSimpleSchedule(s => s.WithIntervalInSeconds(3).WithRepeatCount(3))
.Build();
StdSchedulerFactory factory = new StdSchedulerFactory();
IScheduler scheduler = factory.GetScheduler().GetAwaiter().GetResult();
scheduler.ScheduleJob(jobDetail, trigger).GetAwaiter().GetResult();
scheduler.Start().GetAwaiter().GetResult();
Console.ReadKey();
}
然后在任务中可以接收到这些参数:
public class TestJob : IJob
{
public Task Execute(IJobExecutionContext context)
{
string parm1 = context.JobDetail.JobDataMap["parm1"].ToString();
string parm2 = context.JobDetail.JobDataMap["parm2"].ToString();
return Task.Run(() =>
{
Console.WriteLine($"111打印信息测试...., {DateTime.Now}");
Console.WriteLine($"---获取参数1:{parm1}");
Console.WriteLine($"---获取参数2:{parm2}");
});
}
}
向任务传递参数的场景,不太常用,所以仅供了解即可。一般情况下,我们可以在每次任务执行完成后,将执行结果存储到数据库中或其它高性能存储介质中,用于下一个周期的任务执行时进行使用。
4 调度,任务,时间策略的组合
同一个调度实例,可以调度多个【任务和时间策略的组合】。
假如我们有 3 个任务和 3 个时间策略,我们可以使用一个调度实例,调度 3 个【任务和时间策略的组合】,代码如下所示:
StdSchedulerFactory factory = new StdSchedulerFactory();
IScheduler scheduler = factory.GetScheduler().GetAwaiter().GetResult();
//将刚才创建的 任务实例 和 时间策略实例 相结合,添加到调度实例中
scheduler.ScheduleJob(jobDetail1, trigger1).GetAwaiter().GetResult();
scheduler.ScheduleJob(jobDetail2, trigger2).GetAwaiter().GetResult();
scheduler.ScheduleJob(jobDetail3, trigger3).GetAwaiter().GetResult();
scheduler.Start().GetAwaiter().GetResult();
另外需要注意的是:同一个任务,可以被多个时间策略使用。但是同一个时间策略,不能被多个任务使用。
//同一个任务,可以使用多个时间策略
//此时任务一个时间策略周期到来时,都会触发该任务的执行
await scheduler.ScheduleJob(jobDetail,
new List<ITrigger>() { trigger, trigger2 },
true);
//同一个时间策略,如果运用在多个不同的任务时,程序运行时会抛出异常
scheduler.ScheduleJob(jobDetail1, trigger).GetAwaiter().GetResult();
scheduler.ScheduleJob(jobDetail2, trigger).GetAwaiter().GetResult();
5 调度程序可以添加任务监控事件
我们可以【任务开始事件】、【任务完成事件】、【任务忽略事件】来监控任务的执行,以及编写额外的业务逻辑代码满足我们的特殊需求,具体步骤如下:
首先创建一个任务事件类,实现 Quartz.IJobListener 接口,代码如下:
using Quartz;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace QuartZdemo
{
public class TestJobListener : IJobListener
{
//任务监听类的名称标识
public string Name => "TestJob";
//任务被忽略
//当任务上有 [DisallowConcurrentExecution] 标记时,
//上一个周期结束时任务还没执行完,下一个周期时间到来了,
//那么下个周期会被忽略,就会触发此事件
public Task JobExecutionVetoed(
IJobExecutionContext context,
CancellationToken cancellationToken = default)
{
return Task.Run(() => { Console.WriteLine("此次任务被忽略..."); });
}
//任务执行前触发
public Task JobToBeExecuted(
IJobExecutionContext context,
CancellationToken cancellationToken = default)
{
return Task.Run(() => { Console.WriteLine("任务即将被执行..."); });
}
//任务执行后触发
public Task JobWasExecuted(
IJobExecutionContext context,
JobExecutionException jobException,
CancellationToken cancellationToken = default)
{
return Task.Run(() => { Console.WriteLine("任务已经被执行完..."); });
}
}
}
然后在调度实例中添加此任务事件类的实例即可,完整代码如下:
static void Main(string[] args)
{
IJobDetail jobDetail = JobBuilder.Create<TestJob>().Build();
ITrigger trigger = TriggerBuilder.Create()
.WithSimpleSchedule(s => s.WithIntervalInSeconds(3).WithRepeatCount(3))
.Build();
StdSchedulerFactory factory = new StdSchedulerFactory();
IScheduler scheduler = factory.GetScheduler().GetAwaiter().GetResult();
scheduler.ScheduleJob(jobDetail, trigger).GetAwaiter().GetResult();
//向调度实例中,添加任务事件类的实例
scheduler.ListenerManager.AddJobListener(new TestJobListener());
scheduler.Start().GetAwaiter().GetResult();
Console.ReadKey();
}
执行效果如下图所示:
最后我们可以代码封装到一个方法中,通过 Main 方法来调用:
using Quartz;
using Quartz.Impl;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace QuartZdemo
{
class Program
{
static void Main(string[] args)
{
//初始化并执行调度任务
Init().GetAwaiter().GetResult();
//阻塞控制台窗口,等待输入,防止控制台窗台关闭
Console.ReadKey();
}
public static async Task Init()
{
//任务
IJobDetail jobDetail1 = JobBuilder.Create<TestJob1>().Build();
IJobDetail jobDetail2 = JobBuilder.Create<TestJob2>().Build();
//时间策略
ITrigger trigger1 = TriggerBuilder.Create()
.WithSimpleSchedule(s => s.WithIntervalInSeconds(5).RepeatForever())
.Build();
ITrigger trigger2 = TriggerBuilder.Create()
.WithCronSchedule("0/3 * * * * ? *").Build();
//调度
StdSchedulerFactory factory = new StdSchedulerFactory();
IScheduler scheduler = await factory.GetScheduler();
//添加任务和时间策略的组合
await scheduler.ScheduleJob(jobDetail1, trigger1);
await scheduler.ScheduleJob(jobDetail2, trigger2);
//添加任务事件监听对象实例
scheduler.ListenerManager.AddJobListener(new TestJobListener());
await scheduler.Start();
}
}
}
好了,到此为止,我们已经基本上掌握了 Quartz 定时调度组件的使用,本篇博客主要讲解 Quartz 最核心的使用技术,平时在项目开发中已经足够使用了,有关Quartz 更高级的使用技术,小伙伴们可以自行学习研究。