26. 定时任务/后台任务
以下内容仅限 Furion 2.0.0 +
版本使用。
由于 IIS 有回收的机制,所以定时任务应该采用独立部署,不然经常出现不能触发的情况。查看【Worker Service 章节】
26.1 关于定时任务
顾名思义,定时任务就是在特定的时间或符合某种时间规律执行的任务。通常定时任务有四种时间调度方式:
缓隔时间
方式:延迟多少时间后调配任务,这种方式任务只会被调用一次。间隔时间
方式:每隔一段固定时间调配任务,无间断调用任务。Cron 表达式
方法:通过Cron
表达式计算下一次执行时间进行调配任务,可以配置特定时间范围内执行,也可以无间断执行。自定义下次执行时间
:可以通过各种逻辑运算返回下一次执行时间
26.2 如何实现
Furion
框架提供了两种方式实现定时任务:
SpareTime
静态类:SpareTime
静态类提供SpareTime.Do([options])
方式调用。ISpareTimeWorker
依赖方式:通过自定义类实现ISpareTimeWorker
接口并编写一定规则的方法即可。需要在Startup.cs
中注册services.AddTaskScheduler()
26.3 缓隔方式使用
26.3.1 特定时间后执行
这里演示 3s
后执行
Console.WriteLine("当前时间:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
// timer 是定时器的对象,包含定时器相关信息
// count 表示执行次数,这里只有一次
SpareTime.DoOnce(3000, (timer, count) => {
Console.WriteLine("现在时间:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
});
26.3.2 配置任务信息
SpareTime.DoOnce(3000, (timer, count) => {
Console.WriteLine("现在时间:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
}, "jobName", "描述一下这个任务是干什么的");
jobName
标识任务的唯一标识,通过这个标识可以启动、暂停、销毁任务。
26.3.3 手动启动执行
默认情况下,任务初始化后就立即启动,等待符合的时间就执行,有些时候我们仅仅想初始化时间,不希望立即执行,只需要配置 startNow
即可:
SpareTime.DoOnce(3000, (timer, count) => {
Console.WriteLine("现在时间:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
},"jobName", startNow: false);
// 手动启动执行
SpareTime.Start("jobName");
26.3.4 模拟后台执行
有些时候,我们只需要开启新线程去执行一个任务,比如发短信,发邮件,无需配置。
// 此方法无需主线程等待即可返回,可大大提高性能
SpareTime.DoIt(() => {
// 这里发送短信,发送邮件或记录访问记录
});
还可以指定多长时间后触发,建议 10-1000
毫秒之间:
SpareTime.DoIt(() => {
// 发送短信
}, 100);
26.3.5 ISpareTimeWorker
方式
public class JobWorker : ISpareTimeWorker
{
/// <summary>
/// 3s 后执行
/// </summary>
/// <param name="timer"></param>
/// <param name="count"></param>
[SpareTime(3000, "jobName", DoOnce = true, StartNow = true)]
public void DoSomething(SpareTimer timer, long count)
{
Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
}
/// <summary>
/// 3s 后执行(支持异步)
/// </summary>
/// <param name="timer"></param>
/// <param name="count"></param>
[SpareTime(3000, "jobName", DoOnce = true, StartNow = true)]
public async Task DoSomethingAsync(SpareTimer timer, long count)
{
Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
await Task.CompletedTask;
}
}
需要在 Startup.cs
中注册 services.AddTaskScheduler()
26.4 间隔方式使用
26.4.1 每隔一段时间执行
// 每隔 1s 执行
SpareTime.Do(1000, (timer, count) => {
Console.WriteLine("现在时间:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
Console.WriteLine($"一共执行了:{count} 次");
});
26.4.2 配置任务信息
SpareTime.Do(1000, (timer, count) => {
Console.WriteLine("现在时间:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
Console.WriteLine($"一共执行了:{count} 次");
}, "jobName", "这是一个计时器任务");
26.4.3 手动启动执行
SpareTime.Do(1000, (timer, count) => {
Console.WriteLine("现在时间:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
Console.WriteLine($"一共执行了:{count} 次");
}, "jobName", startNow:false);
SpareTime.Start("jobName");
26.4.4 ISpareTimeWorker
方式
public class JobWorker : ISpareTimeWorker
{
/// <summary>
/// 每隔 3s 执行
/// </summary>
/// <param name="timer"></param>
/// <param name="count"></param>
[SpareTime(3000, "jobName", StartNow = true)]
public void DoSomething(SpareTimer timer, long count)
{
Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
Console.WriteLine($"一共执行了:{count} 次");
}
}
需要在 Startup.cs
中注册 services.AddTaskScheduler()
26.5 Cron
表达式使用
26.5.1 什么是 Cron
表达式
Cron 表达式是一个字符串,字符串以 5
或 6
个空格隔开,分为 6 或 7 个域,每一个域代表一个含义,Cron 有如下两种语法格式:
(1) Seconds Minutes Hours DayofMonth Month DayofWeek Year
(2)Seconds Minutes Hours DayofMonth Month DayofWeek
Cron 从左到右(用空格隔开):秒
分
小时
月份中的日期
月份
星期中的日期
年份
字段 | 允许值 | 允许的特殊字符 |
---|---|---|
秒(Seconds) | 0~59 的整数 | , - \* / 四个字符 |
分(Minutes) | 0~59 的整数 | , - \* / 四个字符 |
小时(Hours) | 0~23 的整数 | , - \* / 四个字符 |
日期(DayofMonth) | 1~31 的整数(但是你需要考虑平闰月的天数) | ,- \* ? / L W C 八个字符 |
月份(Month) | 1~12 的整数或者 JAN-DEC | , - \* / 四个字符 |
星期(DayofWeek) | 1~7 的整数或者 SUN-SAT (1=SUN) | , - \* ? / L C # 八个字符 |
年(可选,留空)(Year) | 1970~2099 | , - \* / 四个字符 |
每一个域都使用数字,但还可以出现如下特殊字符,它们的含义是:
(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
,表示某月的第二个星期三。
26.5.2 常见 Cron
表达式
表达式 | 表达式代表含义 | 格式化 |
---|---|---|
* * * * * | 每分钟 | CronFormat.Standard |
*/1 * * * * | 每分钟 | CronFormat.Standard |
0 0/1 * * * ? | 每分钟 | CronFormat.IncludeSeconds |
0 0 * * * ? | 每小时 | CronFormat.IncludeSeconds |
0 0 0/1 * * ? | 每小时 | CronFormat.IncludeSeconds |
0 23 ? * MON-FRI | 晚上 11:00,周一至周五 | CronFormat.Standard |
* * * * * * | 每秒 | CronFormat.IncludeSeconds |
*/45 * * * * * | 每 45 秒 | CronFormat.IncludeSeconds |
*/5 * * * * | 每 5 分钟 | CronFormat.Standard |
0 0/10 * * * ? | 每 10 分钟 | CronFormat.IncludeSeconds |
0 */5 * * * * | 每 5 分钟 | CronFormat.IncludeSeconds |
30 11 * * 1-5 | 周一至周五上午 11:30 | CronFormat.Standard |
30 11 * * * | 11:30 | CronFormat.Standard |
0-10 11 * * * | 上午 11:00 至 11:10 之间的每一分钟 | CronFormat.Standard |
* * * 3 * | 每分钟,只在 3 月份 | CronFormat.Standard |
* * * 3,6 * | 每分钟,只在 3 月和 6 月 | CronFormat.Standard |
30 14,16 * * * | 下午 02:30 分和 04:30 分 | CronFormat.Standard |
30 6,14,16 * * * | 早上 06:30,下午 02:30 和 04:30 | CronFormat.Standard |
46 9 * * 1 | 早上 09:46,只在星期一 | CronFormat.Standard |
23 12 15 * * | 下午 12:23,在本月的第 15 天 | CronFormat.Standard |
23 12 * JAN * | 下午 12:23,只在 1 月份 | CronFormat.Standard |
23 12 ? JAN * | 下午 12:23,只在 1 月份 | CronFormat.Standard |
23 12 * JAN-FEB * | 下午 12:23,1 月至 2 月 | CronFormat.Standard |
23 12 * JAN-MAR * | 下午 12:23,1 月至 3 月 | CronFormat.Standard |
23 12 * * SUN | 下午 12:23,仅在星期天 | CronFormat.Standard |
*/5 15 * * MON-FRI | 每 5 分钟,下午 0:00 至 03:59,周一至周五 | CronFormat.Standard |
* * * * MON#3 | 每分钟,在月的第三个星期一 | CronFormat.Standard |
* * * * 4L | 每一分钟,在本月的最后一天 | CronFormat.Standard |
*/5 * L JAN * | 每月一次每月 5 分钟,只在 1 月份 | CronFormat.Standard |
30 02 14 * * * | 下午在 02:02:30 | CronFormat.IncludeSeconds |
5-10 * * * * * | 每分钟的 5-10 秒 | CronFormat.IncludeSeconds |
5-10 30-35 10-12 * * * | 10:00 至 12:00 之间的每分钟 5-10 秒,每小时 30-35 分钟 | CronFormat.IncludeSeconds |
30 */5 * * * * | 每分钟的 30 秒,每五分钟 | CronFormat.IncludeSeconds |
0 30 10-13 ? * WED,FRI | 每小时的 30 分钟,下午 10:00 至 01:00 之间,仅在周三和周五 | CronFormat.IncludeSeconds |
10 0/5 * * * ? | 每分钟的 10 秒,每 05 分钟 | CronFormat.IncludeSeconds |
0 0 6 1/1 * ? | 下午 06:00 | CronFormat.IncludeSeconds |
0 5 0/1 * * ? | 一个小时的 05 分 | CronFormat.IncludeSeconds |
0 0 L * * | 每月最后一天上午 00:00 | CronFormat.Standard |
0 0 L-1 * * | 每月最后一天的凌晨 00:00 | CronFormat.Standard |
0 0 3W * * | 每月第 3 个工作日上午 00:00 | CronFormat.Standard |
0 0 LW * * | 在每月的最后一个工作日,上午 00:00 | CronFormat.Standard |
0 0 * * 2L | 本月最后一个星期二上午 00:00 | CronFormat.Standard |
0 0 * * 6#3 | 每月第三个星期六上午 00:00 | CronFormat.Standard |
0 0 ? 1 MON#1 | 1 月第一个星期一 上午 00:00 | CronFormat.Standard |
0 0 3 * * ? | 每天几点执行一次 | CronFormat.IncludeSeconds |
26.5.3 在线生成 Cron
表达式
26.5.4 Macro
标识符
为了方便定义 Cron
表达式,Furion
框架也提供了非常方便的占位符实现常用的时间格式:
占位符 | 对应表达式 | 占位符代表含义 |
---|---|---|
@every_second | * * * * * * | 一秒钟跑一次 |
@every_minute | * * * * * | 在分钟开始时每分钟运行一次 |
@hourly | 0 * * * * | 在小时开始时每小时运行一次 |
@daily | 0 0 * * * | 每天午夜运行一次 |
@midnight | 0 0 * * * | 每天午夜运行一次 |
@weekly | 0 0 * * 0 | 周日上午午夜每周运行一次 |
@monthly | 0 0 1 * * | 每月在每月第一天的午夜运行一次 |
@yearly | 0 0 1 1 * | 每年 1 月 1 日午夜运行一次 |
@annually | 0 0 1 1 * | 每年 1 月 1 日午夜运行一次 |
26.5.5 使用 Cron
表达式
// 每隔 1s 执行
SpareTime.Do("* * * * * *", (timer, count) => {
Console.WriteLine("现在时间:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
Console.WriteLine($"一共执行了:{count} 次");
}, cronFormat: CronFormat.IncludeSeconds);
默认情况下,Furion
框架未启用对 秒
的支持,如需开启,则设置 cronFormat: CronFormat.IncludeSeconds
即可。默认值是 cronFormat: CronFormat.Standard
26.5.6 使用 Macro
占位符
// 每隔 1s 执行
SpareTime.Do("@every_second", (timer, count) => {
Console.WriteLine("现在时间:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
Console.WriteLine($"一共执行了:{count} 次");
});
26.5.7 配置任务信息
SpareTime.Do("* * * * *", (timer, count) => {
Console.WriteLine("现在时间:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
Console.WriteLine($"一共执行了:{count} 次");
}, "cronName", "每分钟执行一次");
26.5.8 手动启动执行
SpareTime.Do("* * * * *", (timer, count) => {
Console.WriteLine("现在时间:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
Console.WriteLine($"一共执行了:{count} 次");
}, "cronName", "每分钟执行一次", startNow: false);
SpareTime.Start("cronName");
26.5.9 ISpareTimeWorker
方式
public class JobWorker : ISpareTimeWorker
{
/// <summary>
/// 每分钟执行
/// </summary>
/// <param name="timer"></param>
/// <param name="count"></param>
[SpareTime("* * * * *", "jobName", StartNow = true)]
public void DoSomething(SpareTimer timer, long count)
{
Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
Console.WriteLine($"一共执行了:{count} 次");
}
/// <summary>
/// 每分钟执行(支持异步)
/// </summary>
/// <param name="timer"></param>
/// <param name="count"></param>
[SpareTime("* * * * *", "jobName", StartNow = true)]
public async Task DoSomethingAsync(SpareTimer timer, long count)
{
Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
Console.WriteLine($"一共执行了:{count} 次");
await Task.CompletedTask;
}
}
需要在 Startup.cs
中注册 services.AddTaskScheduler()
26.6 自定义下次执行时间
有些时候我们需要进行一些业务逻辑,比如数据库查询等操作返回下一次执行时间,这个时候我们可以通过高级自 定义方式。
26.6.1 高级自定义间隔方式
SpareTime.Do(()=>{
// 这里可以查询数据库或进行或进行任何业务逻辑
if(符合逻辑){
return 1000; // 每秒执行
}
else return -1; // 不符合逻辑取消任务
},
(timer,count)=>{
Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
Console.WriteLine($"一共执行了:{count} 次");
});
默认情况下,该自定义会在返回 小于或等于0
时终止任务的执行。但是我们希望该任务不要终止,只要符合条件都一直执行,只需要配置 cancelInNoneNextTime: false
即可
26.6.2 高级自定义 Cron
表达式
SpareTime.Do(()=>{
// 这里可以查询数据库或进行或进行任何业务逻辑
if(符合逻辑){
return DateTimeOffset.Now.AddMinutes(10); // 十分钟后再执行
}
else return null; // 不符合逻辑取消任务
},
(timer,count) => {
Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
Console.WriteLine($"一共执行了:{count} 次");
});
默认情况下,该自定义会在返回 null
时终止任务的执行。但是我们希望该任务不要终止,只要符合条件都一直执行,只需要配置 cancelInNoneNextTime: false
即可,如:
SpareTime.Do(()=>{
// 这里可以查询数据库或进行或进行任何业务逻辑
if(符合逻辑){
return SpareTime.GetCronNextOccurrence("cron 表达式");
}
else return null; // 不符合逻辑继续检查
},
(timer,count) => {
Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
Console.WriteLine($"一共执行了:{count} 次");
}, cancelInNoneNextTime: false);