至于任务调度这个基础功能,重要性不言而喻,大多数业务系统都会用到,世面上有很多成熟的三方库比如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. #平衡树#洛谷 1110 [ZJOI2007]报表统计

    题目 分析 最小值只需要开两棵平衡树,一棵维护所有元素,一棵维护相邻最小值, 对于全局最小值,对于每次插入查找前驱后继更新最小值即可, 相邻最小值,对于每个原数列的数维护它的开头和结尾是什么数, 然后 ...

  2. 组合数学——Min-Max容斥

    Min-Max 容斥,即 $$\max(S)=\sum_{T\in S,T\neq\emptyset}(-1)^{|T|-1}\min(T)$$ 接下来证明上面那个式子是对的.定义 \(S\) 中共有 ...

  3. Rome反序列化链分析

    环境搭建 <dependencies> <dependency> <groupId>junit</groupId> <artifactId> ...

  4. 《深入理解Java虚拟机》读书笔记:HotSpot的算法实现

    HotSpot的算法实现 HotSpot的算法实现概要 1.枚举根节点 由于目前的主流Java虚拟机使用的都是准确式GC(这个概念在第1章介绍Exact VM对Classic VM的改进时讲过),所以 ...

  5. 【原创】win11完美搭建macos13实现IOS自动化测试环境

    虚拟机完美搭建IOS自动化测试环境 一.win系统 1.1 安装虚拟机VM16 如果是macos系统请跳过 详细安装请看压缩包,私信可以提供安装包 1.2 VM解锁macOS 使用的是unlocker ...

  6. RestfulApi 学习笔记——分页和排序(六)

    前言 分页和排序时一些非常常规的操作,同样也有一些我们注意的点. 正文 分页 先来谈及分页. 看下前端传递的参数. public class EmployeeDtoParameters { priva ...

  7. Spark SQL 抽样函数 ——TABLESAMPLE 的坑点

    最近需要实现一段 Spark SQL 逻辑,对数据集进行抽样指定的行数. 由于数据集较大,刚开始的逻辑是,取窗口函数随机排序后 row_number 的前 n 行.但运行速度较慢,所以想起了 TABL ...

  8. 云原生时代 RocketMQ 运维管控的利器 - RocketMQ Operator

    作者 | 刘睿.杜恒 导读:RocketMQ Operator 现已加入 OperatorHub,正式进入 Operator 社区.本文将从实践出发,结合案例来说明,如何通过 RocketMQ Ope ...

  9. 重温设计模式之 Factory

    简介: 创建型模式的核心干将,工厂.简单工厂.抽象工厂,还记得清么,一文回顾和对比下. 作者 | 弥高来源 | 阿里技术公众号 前言 创建型模式的核心干将,工厂.简单工厂.抽象工厂,还记得清么,一文回 ...

  10. [FAQ] MetaMask ALERT: 交易出错. 合约代码执行异常.

    首先确认载入的合约地址是否是最新的,比如 web3 载入的 abi 格式的 json 文件名 正不正确. 其次需要检查合约逻辑是否都正确,以及是否是合约抛出的错误,这两点最好是通过写测试用例来保证. ...