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

完整实现代码:https://github.com/WeihanLi/ActivityReservation/blob/dev/ActivityReservation.Helper/Services/RemoveOverdueReservationService.cs

Memo

使用 redis 这种方式来决定 master 并不是特别可靠,正常结束的没有什么问题,最好还是用比较成熟的服务注册发现框架比较好

Reference

.net core 实现基于 cron 表达式的任务调度的更多相关文章

  1. (转)Java任务调度框架Quartz入门教程指南(四)Quartz任务调度框架之触发器精讲SimpleTrigger和CronTrigger、最详细的Cron表达式范例

    http://blog.csdn.net/zixiao217/article/details/53075009 Quartz的主要接口类是Schedule.Job.Trigger,而触发器Trigge ...

  2. Quartz中Cron表达式使用方法

    Quartz中CronTrigger支持日历相关的反复时间间隔(比方每月第一个周一运行),而不是简单的周期时间间隔. 它的调度规则基于 Cron 表达式. 以下就来说一下Cron表达式的规则及使用方法 ...

  3. 作业调度框架 Quartz 学习笔记(三) -- Cron表达式 (转载)

    前面两篇说的是简单的触发器(SimpleTrigger) , SimpleTrigger 只能处理简单的事件出发,如果想灵活的进行任务的触发,就要请出 CronTrigger 这个重要人物了. Cro ...

  4. 使用Cron表达式创建定时任务

    CronTriggerCronTrigger功能非常强大,是基于日历的作业调度,而SimpleTrigger是精准指定间隔,所以相比SimpleTrigger,CroTrigger更加常用.CroTr ...

  5. 基于Spring的最简单的定时任务实现与配置(三)--番外篇 cron表达式的相关内容

    本来这篇文章是会跟本系列的前两篇文章一起发布的.但是,昨天在找资料总结的时候遇到了一点意外,就延后了一些. 本篇的内容主要参考了 这篇博文:http://www.cnblogs.com/junrong ...

  6. 任务调度Cron表达式及Quartz代码详解

    在线Cron表达式生成器 http://cron.qqe2.com/ cron表达式详解 http://www.cnblogs.com/linjiqin/archive/2013/07/08/3178 ...

  7. Quartz.NET总结(二)CronTrigger和Cron表达式

    Quartz.NET的任务调度,主要就是依靠CronTrigger和Cron表达式.Cron是已经在UNIX存在了很长一段时间,它有着强大和可靠的调度能力.CronTrigger类也正是是基于Cron ...

  8. Spring 定时任务之 @Scheduled cron表达式

    一个基于Spring boot的一个demo: Java配置中开户对Scheduled的支持 import org.springframework.context.annotation.Configu ...

  9. Quartz 用 cron 表达式存放执行计划

    Quartz 用 cron 表达式存放执行计划.引用了 cron 表达式的 CronTrigger 在计划的时间里会与 job 关联上. 1.Quartz cron 表达式支持七个域如下: 名称 是否 ...

随机推荐

  1. Linux搭建基于BIND的DNS服务器

    Linux搭建基于BIND的DNS服务器   实验目标: 通过本实验掌握基于Linux的DNS服务器搭建. 实验步骤: 1.安装BIND 2.防火墙放通DNS服务 3.编辑BIND的主配置文件 4.编 ...

  2. Linux搭建DHCP服务器

    Linux搭建DHCP服务器   实验目标: 通过本实验掌握基于Linux的DHCP服务器搭建技能. 本实验包含内容为yum的认识与使用,磁盘挂载的概念与使用,DHCP原理及配置,systemctl服 ...

  3. 【Go】使用压缩文件优化io (二)

    原文链接: https://blog.thinkeridea.com/201907/go/compress_file_io_optimization2.html 上一篇文章<使用压缩文件优化io ...

  4. POJ 3301:Texas Trip(计算几何+三分)

    http://poj.org/problem?id=3301 题意:在二维平面上有n个点,每个点有一个坐标,问需要的正方形最小面积是多少可以覆盖所有的点. 思路:从第二个样例可以看出,将正方形旋转45 ...

  5. Mybatis 一对多分页踩坑 对collection的分析

    背景描述: 产品和结算对象(结算名和结算金额)是一对多的关系,使用 collection 做一对多配置.但是出现一对多时,数据没有整合至一起,导致一个产品重复出现. class ResponseVo{ ...

  6. php中对象类型与数组之间的转换

    1.刚看视频学习的时候看到一个困扰很久的问题, 有时候我们在进行做项目的时候会碰到的一个小问题.举一个小例子.  获取一个xml文件里面的数据. xml.xml文件如下: <?xml versi ...

  7. git简介 http://msysgit.github.io/

    集中式vs分布式 Linus一直痛恨的CVS及SVN都是集中式的版本控制系统,而Git是分布式版本控制系统,集中式和分布式版本控制系统有什么区别呢? 先说集中式版本控制系统,版本库是集中存放在中央服务 ...

  8. Oracle数据库----查询

    --笛卡尔集select empno,ename, 员工表.deptno, 部门表.deptno, dname from 部门表, 员工表; --添加合适的条件,可以避免笛卡尔集,从而得到正确的多表查 ...

  9. Flask-登录练习

    基于蓝图CBV模式的登录 使用蓝图并用cbv模式完成登录功能 登录成功后跳转到首页 将session保存在liunx上的redis数据库 使用before_request验证是否是登陆用户 蓝图 fr ...

  10. Spring Boot2(十三):整合定时任务发送邮件

    一.前言 主要玩一下SpringBoot的定时任务和发送邮件的功能.定时发送邮件,这在实际生成环境下主要用户系统性能监控时,当超过设定的阙值,就发送邮件通知预警功能.这里只通过简单的写个定时结合邮件通 ...