.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:15
0 15 10 * * ?
每天 10:15
30 15 10 * * ? *
每天 10:15:30
0 15 10 * * ? 2005
2005年每天 10:15
详细信息可以参考:http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html
.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
- http://crontab.org/
- https://en.wikipedia.org/wiki/Cron
- http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html
- https://github.com/WeihanLi/ActivityReservation
.net core 实现基于 cron 表达式的任务调度的更多相关文章
- (转)Java任务调度框架Quartz入门教程指南(四)Quartz任务调度框架之触发器精讲SimpleTrigger和CronTrigger、最详细的Cron表达式范例
http://blog.csdn.net/zixiao217/article/details/53075009 Quartz的主要接口类是Schedule.Job.Trigger,而触发器Trigge ...
- Quartz中Cron表达式使用方法
Quartz中CronTrigger支持日历相关的反复时间间隔(比方每月第一个周一运行),而不是简单的周期时间间隔. 它的调度规则基于 Cron 表达式. 以下就来说一下Cron表达式的规则及使用方法 ...
- 作业调度框架 Quartz 学习笔记(三) -- Cron表达式 (转载)
前面两篇说的是简单的触发器(SimpleTrigger) , SimpleTrigger 只能处理简单的事件出发,如果想灵活的进行任务的触发,就要请出 CronTrigger 这个重要人物了. Cro ...
- 使用Cron表达式创建定时任务
CronTriggerCronTrigger功能非常强大,是基于日历的作业调度,而SimpleTrigger是精准指定间隔,所以相比SimpleTrigger,CroTrigger更加常用.CroTr ...
- 基于Spring的最简单的定时任务实现与配置(三)--番外篇 cron表达式的相关内容
本来这篇文章是会跟本系列的前两篇文章一起发布的.但是,昨天在找资料总结的时候遇到了一点意外,就延后了一些. 本篇的内容主要参考了 这篇博文:http://www.cnblogs.com/junrong ...
- 任务调度Cron表达式及Quartz代码详解
在线Cron表达式生成器 http://cron.qqe2.com/ cron表达式详解 http://www.cnblogs.com/linjiqin/archive/2013/07/08/3178 ...
- Quartz.NET总结(二)CronTrigger和Cron表达式
Quartz.NET的任务调度,主要就是依靠CronTrigger和Cron表达式.Cron是已经在UNIX存在了很长一段时间,它有着强大和可靠的调度能力.CronTrigger类也正是是基于Cron ...
- Spring 定时任务之 @Scheduled cron表达式
一个基于Spring boot的一个demo: Java配置中开户对Scheduled的支持 import org.springframework.context.annotation.Configu ...
- Quartz 用 cron 表达式存放执行计划
Quartz 用 cron 表达式存放执行计划.引用了 cron 表达式的 CronTrigger 在计划的时间里会与 job 关联上. 1.Quartz cron 表达式支持七个域如下: 名称 是否 ...
随机推荐
- kubernetes实战篇之helm使用技巧
系列目录 使用压缩包安装chart 我们使用helm package打包的时候,默认会在当前位置生成一个tgz压缩包,然后helm把它复制到到$HOME/.helm/repository目录下,现在还 ...
- Python 3.6 安装
1. 下载 # 我下载到了 /tmp 目录中 cd /tmp wget https://www.python.org/ftp/python/3.6.0/Python-3.6.0.tgz 2. 安装依赖 ...
- leadcode的Hot100系列--617. 合并二叉树
合并,就是两个树的结构交集部分,数据相加,否则,取非空部分. 所以,这里相当于是对两棵树同时遍历: 如果两棵树节点都不为空,则数据相加, 否则,直接指针把不为空的节点复制过来. 注:这里没有申请内存, ...
- 为什么Python 3.6以后字典有序并且效率更高?
在Python 3.5(含)以前,字典是不能保证顺序的,键值对A先插入字典,键值对B后插入字典,但是当你打印字典的Keys列表时,你会发现B可能在A的前面. 但是从Python 3.6开始,字典是变成 ...
- 关于过两级mux的时序约束的添加(一个非常经典的时序约束问题)
非常开心自己的微信公众号: <数字集成电路设计及EDA教程> 关注者超过了1700 里面主要讲解数字IC前端.后端.DFT.低功耗设计以及验证等相关知识,并且讲解了其中用到的各种EDA工具 ...
- 附录:1-Grain生命周期-译注
Grain Lifecycle Grains are logical entities that always exist, virtually, and have stable logical id ...
- 20131221-Dom练习-第二十六天(未完)
[1] //总结,写代码,一要动脑,理解用脑 //二要练,要动手,要有用身体记忆代码的觉悟,记忆用手 //三学编程最快的方法是,直接接触代码,用脑,用手接触代码 //面向对象的编码方式,对象还是对象, ...
- UVA1327 && POJ1904 King's Quest(tarjan+巧妙建图+强连通分量+缩点)
UVA1327 King's Quest POJ1904 King's Quest 题意: 有n个王子,每个王子都有k个喜欢的妹子,每个王子只能和喜欢的妹子结婚.现有一个匹配表,将每个王子都与一个自己 ...
- 批量替换git目录的远程仓库URL地址脚本
需求: 1. 输入work-dir 工作目录 2. 扫描工作目录中的子目录 3. 对每一个子目录, 判断是否是git repo 4. 确认是git repo, 获取git origin remote- ...
- 使用ML-Agents Toolkit(0.5)训练游戏ai之环境搭建
ML-Agents toolkit目前已经更新到0.5版本了. 要想使用这个Unity插件训练人工智能需要如下软件 1.Anaconda指的是一个开源的Python发行版本,主要是让你的训练环境与其它 ...