至于任务调度这个基础功能,重要性不言而喻,大多数业务系统都会用到,世面上有很多成熟的三方库比如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成功了:

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

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

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

自己动手实现一个轻量无负担的任务调度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. Nancy总结(一)Nancy一个轻量的MVC框架

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

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

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

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

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

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

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

  8. BlockCanary 一个轻量的,非侵入式的性能监控组件(阿里)

    开发者博客: BlockCanary — 轻松找出Android App界面卡顿元凶 开源代码:moduth/blockcanary BlockCanary对主线程操作进行了完全透明的监控,并能输出有 ...

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

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

  10. Cardinal:一个用于移动项目开发的轻量 CSS 框架

    Cardinal 是一个适用于移动项目的 CSS 框架,包含很多有用的默认样式.矢量字体.可重用的模块以及一个简单的响应式模块系统.Cardinal 提供了一种在多种移动设备上实现可伸缩的字体和布局的 ...

随机推荐

  1. JDK14性能管理工具:jmap和jhat使用介绍

    目录 简介 jmap clstats finalizerinfo histo dump jhat 总结 简介 我们在写代码的过程中,经常会遇到内存泄露的问题,比如某个集合中的对象没有被回收,或者内存出 ...

  2. SpringBoot中bean的生命周期

    目录 概述 使用场景 代码演示bean初始化 TestSupport BeanPostProcessorImpl log 代码 概述 Bean 生命周期管理是 Spring Boot 中的关键功能之一 ...

  3. 高可用之战:Redis Sentinal(哨兵模式)

    ★ Redis24篇集合 1 背景 在我们的<Redis高可用之战:主从架构>篇章中,介绍了Redis的主从架构模式,可以有效的提升Redis服务的可用性,减少甚至避免Redis服务发生完 ...

  4. 码力全开!请查收HDC.Together 2023亮点日程

    <主题演讲> <技术交流与互动> <妙趣之旅>

  5. 重新点亮linux 命令树————su和sudo[七]

    前言 简单整理一下su和sudo这两个命令. 正文 su 这个命令是用来切换用户的. 一般我们切换命令su,还是在原来的路径下面,但是如果想要直接回到切换用户的用户目录的话,可以使用su - user ...

  6. pycharm更换主题,pycharm更换皮肤,pycharm更换不同颜色

    1.首先  点击File→进入setting 2. 在settings里面找到appearance 3.选择 Theme,下拉即可修改 4.选择不同的主题,darcula是黑色,其他两个是白色

  7. Vue-flask 展示小电影

    显示小电影 前端Vue <body> <div id="app"> <button @click="handleLoad"> ...

  8. ClkLog自定义事件分析登场

     ClkLog的自定义事件分析功能在大家满满的期待下终于发布了. 这次更新我们添加了[用户关联].[事件采集].[事件分析]三大块功能点. 本次上线的自定义事件分析可以让用户根据自身业务场景创建不同维 ...

  9. HarmonyOS NEXT应用开发—在Native侧实现进度通知功能

    介绍 本示例通过模拟下载场景介绍如何将Native的进度信息实时同步到ArkTS侧. 效果图预览 使用说明 点击"Start Download"按钮后,Native侧启动子线程模拟 ...

  10. 如何将传统 Web 框架迁移部署到 Serverless 架构?

    简介: 与其说 Serverless 架构是一个新的概念,不如说它是一种全新的思路,一种新的编程范式. 与其说 Serverless 架构是一个新的概念,不如说它是一种全新的思路,一种新的编程范式. ...