至于任务调度这个基础功能,重要性不言而喻,大多数业务系统都会用到,世面上有很多成熟的三方库比如Quartz,Hangfire,Coravel

这里我们不讨论三方的库如何使用 而是从0开始自己制作一个简易的任务调度,如果只是到分钟级别的粒度基本够用

技术栈用到了:BackgroundServiceNCrontab

第一步我们定义一个简单的任务约定,不干别的就是一个执行方法:

    public interface IScheduleTask
{
Task ExecuteAsync();
}
public abstract class ScheduleTask : IScheduleTask
{
public virtual Task ExecuteAsync()
{
return Task.CompletedTask;
}
}

第二步定义特性标注任务执行周期等信的metadata

    [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public class ScheduleTaskAttribute(string cron) : Attribute
{
/// <summary>
/// 支持的cron表达式格式 * * * * *:https://en.wikipedia.org/wiki/Cron
/// 最小单位为分钟
/// </summary>
public string Cron { get; set; } = cron;
public string? Description { get; set; }
/// <summary>
/// 是否异步执行.默认false会阻塞接下来的同类任务
/// </summary>
public bool IsAsync { get; set; } = false;
/// <summary>
/// 是否初始化即启动,默认false
/// </summary>
public bool IsStartOnInit { get; set; } = false;
}

第三步我们定义一个调度器约定,不干别的就是判断当前的任务是否可以执行:

    public interface IScheduler
{
/// <summary>
/// 判断当前的任务是否可以执行
/// </summary>
bool CanRun(ScheduleTaskAttribute scheduleMetadata, DateTime referenceTime);
}

好了,基础步骤就完成了,如果我们需要实现配置级别的任务调度或者动态的任务调度 那我们再抽象一个Store:

    public class ScheduleTaskMetadata(Type scheduleTaskType, string cron)
{
public Type ScheduleTaskType { get; set; } = scheduleTaskType;
public string Cron { get; set; } = cron;
public string? Description { get; set; }
public bool IsAsync { get; set; } = false;
public bool IsStartOnInit { get; set; } = false;
}
public interface IScheduleMetadataStore
{
/// <summary>
/// 获取所有ScheduleTaskMetadata
/// </summary>
Task<IEnumerable<ScheduleTaskMetadata>> GetAllAsync();
}

实现一个Configuration级别的Store

    internal class ConfigurationScheduleMetadataStore(IConfiguration configuration) : IScheduleMetadataStore
{
const string Key = "BiwenQuickApi:Schedules"; public Task<IEnumerable<ScheduleTaskMetadata>> GetAllAsync()
{
var options = configuration.GetSection(Key).GetChildren(); if (options?.Any() is true)
{
var metadatas = options.Select(x =>
{
var type = Type.GetType(x[nameof(ConfigurationScheduleOption.ScheduleType)]!);
if (type is null)
throw new ArgumentException($"Type {x[nameof(ConfigurationScheduleOption.ScheduleType)]} not found!"); return new ScheduleTaskMetadata(type, x[nameof(ConfigurationScheduleOption.Cron)]!)
{
Description = x[nameof(ConfigurationScheduleOption.Description)],
IsAsync = string.IsNullOrEmpty(x[nameof(ConfigurationScheduleOption.IsAsync)]) ? false : bool.Parse(x[nameof(ConfigurationScheduleOption.IsAsync)]!),
IsStartOnInit = string.IsNullOrEmpty(x[nameof(ConfigurationScheduleOption.IsStartOnInit)]) ? false : bool.Parse(x[nameof(ConfigurationScheduleOption.IsStartOnInit)]!),
};
});
return Task.FromResult(metadatas);
}
return Task.FromResult(Enumerable.Empty<ScheduleTaskMetadata>());
}
}

然后呢,我们可能需要多任务调度的事件做一些操作或者日志存储.比如失败了该干嘛,完成了回调其他后续业务等.我们再来定义一下具体的事件IEvent,具体可以参考我上一篇文章:

https://www.cnblogs.com/vipwan/p/18184088

    public abstract class ScheduleTaskEvent(IScheduleTask scheduleTask, DateTime eventTime) : IEvent
{
/// <summary>
/// 任务
/// </summary>
public IScheduleTask ScheduleTask { get; set; } = scheduleTask;
/// <summary>
/// 触发时间
/// </summary>
public DateTime EventTime { get; set; } = eventTime;
}
/// <summary>
/// 执行完成
/// </summary>
public sealed class TaskSuccessedEvent(IScheduleTask scheduleTask, DateTime eventTime, DateTime endTime) : ScheduleTaskEvent(scheduleTask, eventTime)
{
/// <summary>
/// 执行结束的时间
/// </summary>
public DateTime EndTime { get; set; } = endTime;
}
/// <summary>
/// 执行开始
/// </summary>
public sealed class TaskStartedEvent(IScheduleTask scheduleTask, DateTime eventTime) : ScheduleTaskEvent(scheduleTask, eventTime);
/// <summary>
/// 执行失败
/// </summary>
public sealed class TaskFailedEvent(IScheduleTask scheduleTask, DateTime eventTime, Exception exception) : ScheduleTaskEvent(scheduleTask, eventTime)
{
/// <summary>
/// 异常信息
/// </summary>
public Exception Exception { get; private set; } = exception;
}

接下来我们再实现基于NCrontab的简易调度器,这个调度器主要是解析Cron表达式判断传入时间是否可以执行ScheduleTask,具体的代码:

    internal class SampleNCrontabScheduler : IScheduler
{
/// <summary>
/// 暂存上次执行时间
/// </summary>
private static ConcurrentDictionary<ScheduleTaskAttribute, DateTime> LastRunTimes = new(); public bool CanRun(ScheduleTaskAttribute scheduleMetadata, DateTime referenceTime)
{
var now = DateTime.Now;
var haveExcuteTime = LastRunTimes.TryGetValue(scheduleMetadata, out var time);
if (!haveExcuteTime)
{
var nextStartTime = CrontabSchedule.Parse(scheduleMetadata.Cron).GetNextOccurrence(referenceTime);
LastRunTimes.TryAdd(scheduleMetadata, nextStartTime); //如果不是初始化启动,则不执行
if (!scheduleMetadata.IsStartOnInit)
return false;
}
if (now >= time)
{
var nextStartTime = CrontabSchedule.Parse(scheduleMetadata.Cron).GetNextOccurrence(referenceTime);
//更新下次执行时间
LastRunTimes.TryUpdate(scheduleMetadata, nextStartTime, time);
return true;
}
return false;
}
}

然后就是核心的BackgroundService了,这里我用的IdleTime心跳来实现,粒度分钟,当然内部也可以封装Timer等实现更复杂精度更高的调度,这里就不展开讲了,代码如下:


internal class ScheduleBackgroundService : BackgroundService
{
private static readonly TimeSpan _pollingTime
#if DEBUG
//轮询20s 测试环境下,方便测试。
= TimeSpan.FromSeconds(20);
#endif
#if !DEBUG
//轮询60s 正式环境下,考虑性能轮询时间延长到60s
= TimeSpan.FromSeconds(60);
#endif
//心跳10s.
private static readonly TimeSpan _minIdleTime = TimeSpan.FromSeconds(10);
private readonly ILogger<ScheduleBackgroundService> _logger;
private readonly IServiceProvider _serviceProvider;
public ScheduleBackgroundService(ILogger<ScheduleBackgroundService> logger, IServiceProvider serviceProvider)
{
_logger = logger;
_serviceProvider = serviceProvider;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var pollingDelay = Task.Delay(_pollingTime, stoppingToken);
try
{
await RunAsync(stoppingToken);
}
catch (Exception ex)
{
//todo:
_logger.LogError(ex.Message);
}
await WaitAsync(pollingDelay, stoppingToken);
}
}
private async Task RunAsync(CancellationToken stoppingToken)
{
using var scope = _serviceProvider.CreateScope();
var tasks = scope.ServiceProvider.GetServices<IScheduleTask>();
if (tasks is null || !tasks.Any())
{
return;
}
//调度器
var scheduler = scope.ServiceProvider.GetRequiredService<IScheduler>();
async Task DoTaskAsync(IScheduleTask task, ScheduleTaskAttribute metadata)
{
if (scheduler.CanRun(metadata, DateTime.Now))
{
var eventTime = DateTime.Now;
//通知启动
_ = new TaskStartedEvent(task, eventTime).PublishAsync(default);
try
{
if (metadata.IsAsync)
{
//异步执行
_ = task.ExecuteAsync();
}
else
{
//同步执行
await task.ExecuteAsync();
}
//执行完成
_ = new TaskSuccessedEvent(task, eventTime, DateTime.Now).PublishAsync(default);
}
catch (Exception ex)
{
_ = new TaskFailedEvent(task, DateTime.Now, ex).PublishAsync(default);
}
}
};
//注解中的task
foreach (var task in tasks)
{
if (stoppingToken.IsCancellationRequested)
{
break;
}
//标注的metadatas
var metadatas = task.GetType().GetCustomAttributes<ScheduleTaskAttribute>(); if (!metadatas.Any())
{
continue;
}
foreach (var metadata in metadatas)
{
await DoTaskAsync(task, metadata);
}
}
//store中的scheduler
var stores = _serviceProvider.GetServices<IScheduleMetadataStore>().ToArray(); //并行执行,提高性能
Parallel.ForEach(stores, async store =>
{
if (stoppingToken.IsCancellationRequested)
{
return;
}
var metadatas = await store.GetAllAsync();
if (metadatas is null || !metadatas.Any())
{
return;
}
foreach (var metadata in metadatas)
{
var attr = new ScheduleTaskAttribute(metadata.Cron)
{
Description = metadata.Description,
IsAsync = metadata.IsAsync,
IsStartOnInit = metadata.IsStartOnInit,
}; var task = scope.ServiceProvider.GetRequiredService(metadata.ScheduleTaskType) as IScheduleTask;
if (task is null)
{
return;
}
await DoTaskAsync(task, attr);
}
});
} private static async Task WaitAsync(Task pollingDelay, CancellationToken stoppingToken)
{
try
{
await Task.Delay(_minIdleTime, stoppingToken);
await pollingDelay;
}
catch (OperationCanceledException)
{
}
}
}

最后收尾阶段我们老规矩扩展一下IServiceCollection:

        internal static IServiceCollection AddScheduleTask(this IServiceCollection services)
{
foreach (var task in ScheduleTasks)
{
services.AddTransient(task);
services.AddTransient(typeof(IScheduleTask), task);
}
//调度器
services.AddScheduler<SampleNCrontabScheduler>();
//配置文件Store:
services.AddScheduleMetadataStore<ConfigurationScheduleMetadataStore>();
//BackgroundService
services.AddHostedService<ScheduleBackgroundService>();
return services;
}
/// <summary>
/// 注册调度器AddScheduler
/// </summary>
public static IServiceCollection AddScheduler<T>(this IServiceCollection services) where T : class, IScheduler
{
services.AddSingleton<IScheduler, T>();
return services;
} /// <summary>
/// 注册ScheduleMetadataStore
/// </summary>
public static IServiceCollection AddScheduleMetadataStore<T>(this IServiceCollection services) where T : class, IScheduleMetadataStore
{
services.AddSingleton<IScheduleMetadataStore, T>();
return services;
}

老规矩我们来测试一下:

    //通过特性标注的方式执行:
[ScheduleTask(Constants.CronEveryMinute)] //每分钟一次
[ScheduleTask("0/3 * * * *")]//每3分钟执行一次
public class KeepAlive(ILogger<KeepAlive> logger) : IScheduleTask
{
public async Task ExecuteAsync()
{
//执行5s
await Task.Delay(TimeSpan.FromSeconds(5));
logger.LogInformation("keep alive!");
}
}
public class DemoConfigTask(ILogger<DemoConfigTask> logger) : IScheduleTask
{
public Task ExecuteAsync()
{
logger.LogInformation("Demo Config Schedule Done!");
return Task.CompletedTask;
}
}

通过配置文件的方式配置Store:

{
"BiwenQuickApi": {
"Schedules": [
{
"ScheduleType": "Biwen.QuickApi.DemoWeb.Schedules.DemoConfigTask,Biwen.QuickApi.DemoWeb",
"Cron": "0/5 * * * *",
"Description": "Every 5 mins",
"IsAsync": true,
"IsStartOnInit": false
},
{
"ScheduleType": "Biwen.QuickApi.DemoWeb.Schedules.DemoConfigTask,Biwen.QuickApi.DemoWeb",
"Cron": "0/10 * * * *",
"Description": "Every 10 mins",
"IsAsync": false,
"IsStartOnInit": true
}
]
}
}

我们还可以实现自己的Store,这里以放到内存为例,如果有兴趣 你可以可以自行开发一个面板管理:

    public class DemoStore : IScheduleMetadataStore
{
public Task<IEnumerable<ScheduleTaskMetadata>> GetAllAsync()
{
//模拟从数据库或配置文件中获取ScheduleTaskMetadata
IEnumerable<ScheduleTaskMetadata> metadatas =
[
new ScheduleTaskMetadata(typeof(DemoTask),Constants.CronEveryNMinutes(2))
{
Description="测试的Schedule"
},
];
return Task.FromResult(metadatas);
}
}
//然后注册这个Store:
builder.Services.AddScheduleMetadataStore<DemoStore>();

所有的一切都大功告成,最后我们来跑一下Demo,成功了:

当然这里是自己的固定思维设计的一个简约版,还存在一些不足,欢迎板砖轻拍指正!

2024/05/16更新:

提供同一时间单一运行中的任务实现

/// <summary>
/// 模拟一个只能同时存在一个的任务.一分钟执行一次,但是耗时两分钟.
/// </summary>
/// <param name="logger"></param>
[ScheduleTask(Constants.CronEveryMinute, IsStartOnInit = true)]
public class OnlyOneTask(ILogger<OnlyOneTask> logger) : OnlyOneRunningScheduleTask
{
public override Task OnAbort()
{
logger.LogWarning($"[{DateTime.Now}]任务被打断.因为有一个相同的任务正在执行!");
return Task.CompletedTask;
} public override async Task ExecuteAsync()
{
var now = DateTime.Now;
//模拟一个耗时2分钟的任务
await Task.Delay(TimeSpan.FromMinutes(2));
logger.LogInformation($"[{now}] ~ {DateTime.Now} 执行一个耗时两分钟的任务!");
}
}

源代码我发布到了GitHub,欢迎star! https://github.com/vipwan/Biwen.QuickApi

https://github.com/vipwan/Biwen.QuickApi/tree/master/Biwen.QuickApi/Scheduling

NETCore中实现一个轻量无负担的极简任务调度ScheduleTask的更多相关文章

  1. 对 JDBC 做一个轻量封装,待完善。。。

    对 JDBC 做一个轻量地封装,顺便复习,熟悉sql,io,util,lang.Reflect等包的使用,泛型的使用,待完善... package com.webproj.utils; import ...

  2. vue-concise-slider 一个轻量的vue幻灯片组件

    vue-concise-slider 一个轻量的vue幻灯片组件 阅读 541 收藏 35 2017-07-03 原文链接:github.com 外卖订单处理有烦恼?试试美团点评餐饮开放平台吧,可实现 ...

  3. Day.js 是一个轻量的处理时间和日期的 JavaScript 库

    Day.js 是一个轻量的处理时间和日期的 JavaScript 库,和 Moment.js 的 API 设计保持完全一样. 如果您曾经用过 Moment.js, 那么您已经知道如何使用 Day.js ...

  4. 在Web应用中接入微信支付的流程之极简清晰版

    在Web应用中接入微信支付的流程之极简清晰版 背景: 在Web应用中接入微信支付,我以为只是调用几个API稍作调试即可. 没想到微信的API和官方文档里隐坑无数,致我抱着怀疑人生的心情悲愤踩遍了丫们布 ...

  5. 在Web应用中接入微信支付的流程之极简清晰版 (转)

    在Web应用中接入微信支付的流程之极简清晰版 背景: 在Web应用中接入微信支付,我以为只是调用几个API稍作调试即可. 没想到微信的API和官方文档里隐坑无数,致我抱着怀疑人生的心情悲愤踩遍了丫们布 ...

  6. Nancy总结(一)Nancy一个轻量的MVC框架

    Nancy是一个基于.net 和Mono 构建的HTTP服务框架,是一个非常轻量级的web框架. 设计用于处理 DELETE, GET, HEAD, OPTIONS, POST, PUT 和 PATC ...

  7. 曹工说Tomcat4:利用 Digester 手撸一个轻量的 Spring IOC容器

    一.前言 一共8个类,撸一个IOC容器.当然,我们是很轻量级的,但能够满足基本需求.想想典型的 Spring 项目,是不是就是各种Service/DAO/Controller,大家互相注入,就组装成了 ...

  8. 在项目管理中如何保持专注,分享一个轻量的时间管理工具【Flow Mac版 - 追踪你在Mac上的时间消耗】

    在项目管理和团队作业中,经常面临的问题就是时间管理和优先级管理发生问题,项目被delay,团队工作延后,无法达到预期目标. 这个仿佛是每个人都会遇到的问题,特别是现在这么多的内容软件来分散我们的注意力 ...

  9. 自己写一个轻量的JqueryGrid组件

    接触mvc不久,突然没有了viewstate和服务端控件处处都觉得不顺手,很多在webform时不必要考虑的问题都出现在眼前,这其中分页时查询条件保持的问题又是最让我头疼的事情,权衡再三,决定用aja ...

  10. 使用Hexo建立一个轻量、简易、高逼格的博客

    原文转载自「刘悦的技术博客」https://v3u.cn/a_id_93 在之前的一篇文章中,介绍了如何使用Hugo在三分钟之内建立一个简单的个人博客系统,它是基于go lang的,其实,市面上还有一 ...

随机推荐

  1. 获取 BSTR 字符串长度

    BSTR a = L"hello world"; int len = SysStringByteLen(a);

  2. Taurus.MVC 性能压力测试(ap 压测 和 linux 下wrk 压测):.NET Core 版本

    前言: 最近的 Taurus.MVC 版本,对性能这一块有了不少优化,因此准备进行一下压测,来测试并记录一下 Taurus.MVC 框架的性能,以便后续持续优化改进. 今天先压测 .NET Core  ...

  3. .Net 代码分析工具对比 visual studio 2022 current

    目录 原因 背景知识 名词解释 分析器 分析器在IDE里 目标 查找思路及过程 CodeMaid Roslyn StyleCop.Analyzer StyleCop? StyleCop.Analyze ...

  4. UJCMS 9.1.0 发布,国内开源 java cms

    许可协议从GPL-2改为Apache-2.0,更宽松的协议,方便用户将系统集成到自身的应用中. 修复了已知bug,系统更加稳定. 升级日志(9.1.0) 修复前台全文搜索没有结果 web.xml se ...

  5. 重走py 之路 ——字典和集合(二)

    前言 python 中有6大标准类型: 数字(Number) 字符串(String) 列表(List) 元组(Tumple) 集合(Set) 字典(Dictionary) 前面已经介绍了上面4种,还有 ...

  6. 实用的SpringBoot生成License方案

    前言 License指的是版权许可证,当我们开发完系统后,如果不想让用户一直白嫖使用,比如说按时间续费,License的作用就有了. 我们可以给系统指定License的有效期,控制系统的可用时间. 那 ...

  7. 顺通鞋业MES生产工单管理系统软件

    顺通鞋业MES管理系统的"生产执行"是办公室和车间信息交互的枢纽,是一款针对大型鞋业生产企业开发的可配置化智能制造管理系统.工人可以通过车间终端(如安装在机器旁的固定工业触摸屏或移 ...

  8. 【笔记】rocketMQ了解

    记录rocketMQ 忘了从哪儿保存的图了 原图链接:https://www.jianshu.com/p/2838890f3284

  9. 力扣38(java)-外观数列(中等)

    题目: 给定一个正整数 n ,输出外观数列的第 n 项. 「外观数列」是一个整数序列,从数字 1 开始,序列中的每一项都是对前一项的描述. 你可以将其视作是由递归公式定义的数字字符串序列: count ...

  10. 源码解读:KubeVela 是如何将 appfile 转换为 K8s 特定资源对象的

    简介: KubeVela 是一个简单易用又高度可扩展的云原生应用管理引擎,是基于 Kubernetes 及阿里云与微软云共同发布的云原生应用开发模型 OAM 构建.本文主要目的是探索 KubeVela ...