.net core 实现基于 cron 表达式的任务调度
.net core 实现基于 cron 表达式的任务调度
intro
上次我们实现了一个简单的基于 timer 的定时任务,详细信息可以看。
但是使用过程中慢慢发现这种方式可能并不太合适,有些任务可能只希望在某个时间段内执行,只使用 timer 就显得不是那么灵活了,希望可以像 quartz 那样指定一个 cron 表达式来指定任务的执行时间。
cron 表达式介绍
cron 常见于unix和类unix的操作系统之中,用于设置周期性被执行的指令。该命令从标准输入设备读取指令,并将其存放于“crontab”文件中,以供之后读取和执行。该词来源于希腊语 chronos(χρόνος),原意是时间。
通常,
crontab
储存的指令被守护进程激活,crond
常常在后台运行,每一分钟检查是否有预定的作业需要执行。这类作业一般称为cron jobs。
cron 可以比较准确的描述周期性执行任务的执行时间,标准的 cron 表达式是五位:
30 4 * * ?
五个位置上的值分别对应 分钟/小时/日期/月份/周(day of week)
现在有一些扩展,有6位的,也有7位的,6位的表达式第一个对应的是秒,7个的第一个对应是秒,最后一个对应的是年份
0 0 12 * * ?
每天中午12点0 15 10 ? * *
每天 10:150 15 10 * * ?
每天 10:1530 15 10 * * ? *
每天 10:15:300 15 10 * * ? 2005
2005年每天 10:15
详细信息可以参考:
.net core cron service
cron 解析库 使用的是 https://github.com/hangfireio/cronos
,支持五位/六位,暂不支持年份的解析(7位)
基于 backgroundservice
的 cron 定时服务,实现如下:
public abstract class cronscheduleservicebase : backgroundservice { /// <summary> /// job cron trigger expression /// refer to: http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html /// </summary> public abstract string cronexpression { get; } protected abstract bool concurrentallowed { get; } protected readonly ilogger logger; private readonly string jobclientscache = "jobclientshash"; protected cronscheduleservicebase(ilogger logger) { logger = logger; } protected abstract task processasync(cancellationtoken cancellationtoken); protected override async task executeasync(cancellationtoken stoppingtoken) { { var next = cronhelper.getnextoccurrence(cronexpression); while (!stoppingtoken.iscancellationrequested && next.hasvalue) { var now = datetimeoffset.utcnow; if (now >= next) { if (concurrentallowed) { _ = processasync(stoppingtoken); next = cronhelper.getnextoccurrence(cronexpression); if (next.hasvalue) { logger.loginformation("next at {next}", next); } } else { var machinename = redismanager.hashclient.getorset(jobclientscache, gettype().fullname, () => environment.machinename); // try get job master if (machinename == environment.machinename) // ismaster { using (var locker = redismanager.getredlockclient($"{gettype().fullname}_cronservice")) { // redis 互斥锁 if (await locker.trylockasync()) { // 执行 job await processasync(stoppingtoken); next = cronhelper.getnextoccurrence(cronexpression); if (next.hasvalue) { logger.loginformation("next at {next}", next); await task.delay(next.value - datetimeoffset.utcnow, stoppingtoken); } } else { logger.loginformation($"failed to acquire lock"); } } } } } else { // needed for graceful shutdown for some reason. // 1000ms so it doesn't affect calculating the next // cron occurence (lowest possible: every second) await task.delay(1000, stoppingtoken); } } } } public override task stopasync(cancellationtoken cancellationtoken) { redismanager.hashclient.remove(jobclientscache, gettype().fullname); // unregister from jobclients return base.stopasync(cancellationtoken); } }
因为网站部署在多台机器上,所以为了防止并发执行,使用 redis 做了一些事情,job执行的时候尝试获取 redis 中 job 对应的 master 的 hostname,没有的话就设置为当前机器的 hostname,在 job 停止的时候也就是应用停止的时候,删除 redis 中当前 job 对应的 master,job执行的时候判断是否是 master 节点,是 master 才执行job,不是 master 则不执行。完整实现代码:https://github.com/weihanli/activityreservation/blob/dev/activityreservation.helper/services/cronscheduleservicebase.cs#l11
定时 job 示例:
public class removeoverduereservationservice : cronscheduleservicebase { private readonly iserviceprovider _serviceprovider; private readonly iconfiguration _configuration; public removeoverduereservationservice(ilogger<removeoverduereservationservice> logger, iserviceprovider serviceprovider, iconfiguration configuration) : base(logger) { _serviceprovider = serviceprovider; _configuration = configuration; } public override string cronexpression => _configuration.getappsetting("removeoverduereservationcron") ?? "0 0 18 * * ?"; protected override bool concurrentallowed => false; protected override async task processasync(cancellationtoken cancellationtoken) { using (var scope = _serviceprovider.createscope()) { var reservationrepo = scope.serviceprovider.getrequiredservice<iefrepository<reservationdbcontext, reservation>>(); await reservationrepo.deleteasync(reservation => reservation.reservationstatus == 0 && (reservation.reservationfordate < datetime.today.adddays(-3))); } } }
memo
使用 redis 这种方式来决定 master 并不是特别可靠,正常结束的没有什么问题,最好还是用比较成熟的服务注册发现框架比较好
reference
推荐阅读
-
.net core 实现基于 cron 表达式的任务调度
-
C#/.NET/.NET Core定时任务调度的方法或者组件有哪些--Timer,FluentScheduler还是...
-
.net core 实现基于 JSON 的多语言
-
.net core 基于 IHostedService 实现定时任务
-
控制台基于Quartz.Net组件实现定时任务调度(一)
-
asp.net core MVC之实现基于token的认证
-
.net core 基于Hangfire+Mysql持久化实现定时任务配置方法
-
ASP.NET Core基于微软微服务eShopOnContainer事件总线EventBus的实现
-
.NET Core基于Generic Host实现后台任务方法教程
-
.NET或.NET Core Web APi基于tus协议实现断点续传的示例