自己造轮子是一件苦差事。 现在,您可以专注于业务开发,仅需集成 ⭐️Furion⭐️ 即可。
Skip to main content

26. 定时任务/后台任务

版本说明

以下内容仅限 Furion 2.0.0 + 版本使用。

IIS 部署说明

由于 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.Completed;
}
}

需要在 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 表达式是一个字符串,字符串以 56 个空格隔开,分为 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)?:只能用在 DayofMonthDayofWeek 两个域。它也匹配域的任意值,但实际不会。因为 DayofMonthDayofWeek 会相互影响。例如想在每月的 20 日触发调度,不管 20 日到底是星期几,则只能使用如下写法: 13 13 15 20 _ ?, 其中最后一位只能用,而不能使用_,如果使用*表示不管星期几都会触发,实际上并不是这样。

(3)-:表示范围。例如在 Minutes 域使用 5-20,表示从 5 分到 20 分钟每分钟触发一次

(4)/:表示起始时间开始触发,然后每隔固定时间触发一次。例如在 Minutes 域使用 5/20,则意味着 5 分钟触发一次,而 25,45 等分别触发一次.

(5),:表示列出枚举值。例如:在 Minutes 域使用 5,20,则意味着在 520 分每分钟触发一次。

(6)L:表示最后,只能出现在 DayofWeekDayofMonth 域。如果在 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:30CronFormat.Standard
30 11 * * *11:30CronFormat.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:30CronFormat.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:30CronFormat.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:00CronFormat.IncludeSeconds
0 5 0/1 * * ?一个小时的 05 分CronFormat.IncludeSeconds
0 0 L * *每月最后一天上午 00:00CronFormat.Standard
0 0 L-1 * *每月最后一天的凌晨 00:00CronFormat.Standard
0 0 3W * *每月第 3 个工作日上午 00:00CronFormat.Standard
0 0 LW * *在每月的最后一个工作日,上午 00:00CronFormat.Standard
0 0 * * 2L本月最后一个星期二上午 00:00CronFormat.Standard
0 0 * * 6#3每月第三个星期六上午 00:00CronFormat.Standard
0 0 ? 1 MON#11 月第一个星期一上午 00:00CronFormat.Standard

26.5.3 在线生成 Cron 表达式

https://cron.qqe2.com/

26.5.4 Macro 标识符

为了方便定义 Cron 表达式,Furion 框架也提供了非常方便的占位符实现常用的时间格式:

占位符对应表达式占位符代表含义
@every_second* * * * * *一秒钟跑一次
@every_minute* * * * *在分钟开始时每分钟运行一次
@hourly0 * * * *在小时开始时每小时运行一次
@daily0 0 * * *每天午夜运行一次
@midnight0 0 * * *每天午夜运行一次
@weekly0 0 * * 0周日上午午夜每周运行一次
@monthly0 0 1 * *每月在每月第一天的午夜运行一次
@yearly0 0 1 1 *每年 1 月 1 日午夜运行一次
@annually0 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);
关于 CronFormat

默认情况下,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.Completed;
}
}

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);

26.7 ISpareTimeWorker 说明

除了上面的 SpareTime 静态类方式,Furion 框架还提供了 ISpareTimeWorker 方式,使用该方式非常简单,只需要自定义一个公开且非抽象非静态类并实现 ISpareTimeWorker 即可。

在该类中定义的任务方法需满足以下规则:

  • 必须是公开且实例方法
  • 该方法必须返回 void 且提供 SpareTimerlong 两个参数
  • 必须贴有 [SpareTime] 特性

如:

public class JobWorker : ISpareTimeWorker
{
// 每隔一秒执行,且立即启动
[SpareTime(1000, "jobName1", StartNow = true)]
public void DoSomething1(SpareTimer timer, long count)
{
Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
Console.WriteLine($"一共执行了:{count} 次");
}

// 每分钟执行,且立即启动
[SpareTime("* * * * *", "jobName2", StartNow = true)]
public void DoSomething2(SpareTimer timer, long count)
{
Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
Console.WriteLine($"一共执行了:{count} 次");
}

// 每秒执行,且等待启动
[SpareTime("* * * * * *", "jobName3",CronFormat = CronFormat.IncludeSeconds, StartNow = false)]
public void DoSomething3(SpareTimer timer, long count)
{
Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
Console.WriteLine($"一共执行了:{count} 次");
}

// 每秒执行一次,每分钟也执行一次
[SpareTime(1000, "jobName4", StartNow = true)]
[SpareTime("* * * * *", "jobName5", StartNow = true)]
public void DoSomething4(SpareTimer timer, long count)
{
Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
Console.WriteLine($"一共执行了:{count} 次");
}

// 只执行一次
[SpareTime(1000, "jobName5", StartNow = true, DoOnce = true)]
public void DoSomething5(SpareTimer timer, long count)
{
Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
Console.WriteLine($"一共执行了:{count} 次");
}

// 读取配置文件,通过 #(配置路径)
[SpareTime("#(MyJob:Time)", "jobName6", StartNow = true, DoOnce = true)]
public void DoSomething5(SpareTimer timer, long count)
{
Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
Console.WriteLine($"一共执行了:{count} 次");
}

// 支持异步
[SpareTime(1000, "jobName1", 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.Completed;
}
}
关于依赖注入

ISpareTimeWorker 接口主要是用来查找定时器对象的,也就是它的实现类并未提供依赖注入功能,所以在实现类并不支持构造函数注入依赖项。

26.7.1 [SpareTime] 特性

[SpareTime] 支持以下配置属性

  • Interval:间隔时间, double 类型
  • CronExpressionCron 表达式,string 类型
  • WorkerName:任务唯一标识,string 类型,必填
  • Description:任务描述,string 类型
  • DoOnce:是否只执行一次,bool 类型,默认 false
  • StartNow:是否立即启动,默认 false
  • CronFormatCron 表达式格式化方式,CronFormat 枚举类型,默认 CronFormat.Standard
  • ExecuteType:配置任务执行方式,SpareTimeExecuteTypes 枚举类型,默认 SpareTimeExecuteTypes.Parallel

26.8 SpareTime 静态类

SpareTime 静态类提供了一些方法方便初始化和管理任务

26.8.1 初始化任务

// 开启间隔任务
SpareTime.Do(interval, [options]);

// 开启 Cron 表达式任务
SpareTime.Do(expression, [options]);

// 只执行一次任务
SpareTime.DoOnce(interval, [options]);

// 实现自定义任务
SpareTime.Do(()=>{
return DateTime.Now.AddMinutes(10);
},[options]);

26.8.2 实现后台执行

// 实现后台执行
SpareTime.DoIt(()=>{});

26.8.3 开始一个任务

SpareTime.Start("任务标识");

26.8.4 暂停一个任务

SpareTime.Stop("任务标识");
// 还可以标记一个任务执行失败
SpareTime.Stop("任务标识", true);

26.8.5 取消一个任务

SpareTime.Cancel("任务名称");

26.8.6 销毁所有任务

SpareTime.Dispose();

26.8.7 获取所有任务

var workers = SpareTime.GetWorkers();

26.8.8 获取单个任务

var worker = SpareTime.GetWorker("workerName");

26.8.9 解析 Cron 表达式

var nextTime = SpareTime.GetCronNextOccurrence("* * * * *");

26.9 并行串行执行方式

Furion 框架提供了任务两种执行方式 并行串行

  • 并行:无需等待上一次任务完成,默认值
  • 串行:需等待上一次任务完成

26.9.1 SpareTime 静态方式指定

SpareTime.Do(1000, (t, i) =>
{
Thread.Sleep(5000); // 模拟执行耗时任务
Console.WriteLine($"{t.WorkerName} -{t.Description} - {DateTime.Now:yyyy-MM-dd HH:mm:ss} - {i}");
}, "serialName", "模拟串行任务", executeType: SpareTimeExecuteTypes.Serial);

26.9.2 ISpareTimeWorker 方式

[SpareTime(1000, "jobName1", StartNow = true, ExecuteType = SpareTimeExecuteTypes.Serial)]
public void DoSomething1(SpareTimer timer, long count)
{
Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
Console.WriteLine($"一共执行了:{count} 次");
}

26.10 任务异常处理

有些时候我们可能在执行任务过程中出现异常,Furion 也提供了属性判断是否有异常和异常信息,方便记录到日志中,如:

SpareTime.Do(1000, (t, c) =>
{
// 判断是否有异常
if (t.Exception.Any())
{
Console.WriteLine(t.Exception.Values.LastOrDefault()?.Message);
}
// 执行第三次抛异常
if (c > 2)
{
throw Oops.Oh("抛异常" + c);
}
else
{
Console.WriteLine($"{t.WorkerName} -{t.Description} - {DateTime.Now:yyyy-MM-dd HH:mm:ss} - {c}");
}
}, "exceptionJob");
特别说明

如果一个任务错误次数达 10次 则任务将自动停止,并标记任务状态为 Failed

26.11 如何在任务中解析对象

有些时候我们需要在任务中进行数据库操作或解析服务,这时候我们只需要创建一个新的作用域即可

26.11.1 SpareTime 静态类中

SpareTime.Do(1000, (timer,count) => {
Scoped.Create((_, scope) =>
{
var services = scope.ServiceProvider;

// 获取数据库上下文
var dbContext = Db.GetDbContext(services);

// 获取仓储
var respository = Db.GetRepository<Person>(services);

// 解析其他服务
var otherService = services.GetService<XXX>();
var otherService2 = App.GetService<XXX>(services);
});
}, "任务标识");

26.11.2 ISpareTimeWorker 方式

[SpareTime(1000, "jobName1", StartNow = true, ExecuteType = SpareTimeExecuteTypes.Serial)]
public void DoSomething1(SpareTimer timer, long count)
{
Scoped.Create((_, scope) =>
{
var services = scope.ServiceProvider;

// 获取数据库上下文
var dbContext = Db.GetDbContext(services);

// 获取仓储
var respository = Db.GetRepository<Person>(services);

// 解析其他服务
var otherService = services.GetService<XXX>();
var otherService2 = App.GetService<XXX>(services);
});
}
数据库操作注意

如果作用域中对数据库有任何变更操作,需手动调用 SaveChanges 或带 Now 结尾的方法。也可以使用 Scoped.CreateUow(handler) 代替。

26.12 在 BackgroundService 中使用

BackgroundService.NET Core 3 之后提供的轻量级后台任务,同时可以发布到 Windows 服务和 Linux 守护进程中。

26.12.1 间隔执行方式

using Furion.TaskScheduler;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace WorkerService1
{
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;

public Worker(ILogger<Worker> logger)
{
_logger = logger;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// 间隔执行任务
await SpareTime.DoAsync(1000, () =>
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
}, stoppingToken);
}
}
}
}

26.12.2 Cron 表达式执行方式

using Furion.TaskScheduler;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace WorkerService1
{
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;

public Worker(ILogger<Worker> logger)
{
_logger = logger;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// 执行 Cron 表达式任务
await SpareTime.DoAsync("*/5 * * * * *", () =>
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
}, stoppingToken, CronFormat.IncludeSeconds);
}
}
}
}

26.13 定时任务监听器

Furion v2.18+ 版本之后新增了定时任务监听器 ISpareTimeListener,通过监听器可以实现所有定时任务的状态。如,创建一个 单例 的监听器 SpareTimeListener

using Furion.DependencyInjection;
using Furion.TaskScheduler;
using System;
using System.Threading.Tasks;

namespace Furion.Core
{
public class SpareTimeListener : ISpareTimeListener, ISingleton
{
/// <summary>
/// 监听所有任务
/// </summary>
/// <param name="executer"></param>
/// <returns></returns>
public Task OnListener(SpareTimerExecuter executer)
{
switch (executer.Status)
{
// 执行开始通知
case 0:
Console.WriteLine($"{executer.Timer.WorkerName} 任务开始通知");
break;
// 任务执行之前通知
case 1:
Console.WriteLine($"{executer.Timer.WorkerName} 执行之前通知");
break;
// 执行成功通知
case 2:
Console.WriteLine($"{executer.Timer.WorkerName} 执行成功通知");
break;
// 任务执行失败通知
case 3:
Console.WriteLine($"{executer.Timer.WorkerName} 执行失败通知");
break;
// 任务执行停止通知
case -1:
Console.WriteLine($"{executer.Timer.WorkerName} 执行停止通知");
break;
// 任务执行取消通知
case -2:
Console.WriteLine($"{executer.Timer.WorkerName} 执行取消通知");
break;
default:
break;
}

return Task.CompletedTask;
}
}
}

26.13.1 SpareTimerExecuter 属性说明

  • TimerSpareTimer 定时器对象
  • Status:监听状态
    • 0:任务开始
    • 1:执行之前
    • 2:执行成功
    • 3:执行失败
    • -1:任务停止
    • -2:任务取消

26.14 IIS 部署回收设置

如果在项目中使用了定时任务且部署到 IIS 中,那么需要设置 IIS 禁止回收,如:

部署建议

建议定时任务采用 Worker Service 独立部署方式,不应依托 Web 项目进程中。查看【 Worker Service】章节

26.15 反馈与建议

与我们交流

给 Furion 提 Issue