.NET 纯原生实现 Cron 定时任务执行,未依赖第三方组件 (Timer 优化版)
在上个月写过一篇 .NET 纯原生实现 Cron 定时任务执行,未依赖第三方组件 的文章,当时 CronSchedule 的实现是使用了,每个服务都独立进入到一个 while 循环中,进行定期扫描是否到了执行时间来实现的,但是那个逻辑有些问题,经过各位朋友的测试,发现当多个任务的时候存在一定概率不按照计划执行的情况。
感谢各位朋友的积极探讨,多交流一起进步。之前那个 while 循环的逻辑每循环一次 Task.Delay 1000 毫秒,无限循环,多个任务的时候还会同时有多个循环任务,确实不够好。
所以决定重构 CronSchedule 的实现,采用全局使用一个 Timer 的形式,每隔 1秒钟扫描一次任务队列看看是否有需要执行的任务,整体的实现思路还是之前的,如果没有看过之前那篇文章的建议先看一下,本片主要针对调整部分进行说明 .NET 纯原生实现 Cron 定时任务执行,未依赖第三方组件 ,主要调整了 CronSchedule.cs
using Common;
using System.Reflection;
namespace TaskService.Libraries
{
public class CronSchedule
{
private static List<ScheduleInfo> scheduleList = new();
private static Timer mainTimer;
public static void Builder(object context)
{
var taskList = context.GetType().GetMethods().Where(t => t.GetCustomAttributes(typeof(CronScheduleAttribute), false).Length > 0).ToList();
foreach (var action in taskList)
{
string cron = action.CustomAttributes.Where(t => t.AttributeType == typeof(CronScheduleAttribute)).FirstOrDefault()!.NamedArguments.Where(t => t.MemberName == "Cron" && t.TypedValue.Value != null).Select(t => t.TypedValue.Value!.ToString()).FirstOrDefault()!;
scheduleList.Add(new ScheduleInfo
{
CronExpression = cron,
Action = action,
Context = context
});
}
if (mainTimer == default)
{
mainTimer = new(Run, null, 0, 1000);
}
}
private static void Run(object? state)
{
var nowTime = DateTime.Parse(DateTimeOffset.UtcNow.ToString("yyyy-MM-dd HH:mm:ss"));
foreach (var item in scheduleList)
{
if (item.LastTime != null)
{
var nextTime = DateTime.Parse(CronHelper.GetNextOccurrence(item.CronExpression, item.LastTime.Value).ToString("yyyy-MM-dd HH:mm:ss"));
if (nextTime == nowTime)
{
item.LastTime = DateTimeOffset.Now;
_ = Task.Run(() =>
{
item.Action.Invoke(item.Context, null);
});
}
}
else
{
item.LastTime = DateTimeOffset.Now.AddSeconds(5);
}
}
}
private class ScheduleInfo
{
public string CronExpression { get; set; }
public MethodInfo Action { get; set; }
public object Context { get; set; }
public DateTimeOffset? LastTime { get; set; }
}
}
[AttributeUsage(AttributeTargets.Method)]
public class CronScheduleAttribute : Attribute
{
public string Cron { get; set; }
}
}
这里的逻辑改为了注入任务时将 mainTimer 实例化启动,每一秒钟执行1次 Run方法,Run 方法内部用于 循环检测 scheduleList 中的任务,如果时间符合,则启动一个 Task 去执行对应的 Action,这样全局不管注册多少个服务,也只有一个 Timer 在循环运行,相对之前的 CronSchedule 实现相对更好一点。
使用的时候方法基本没怎么改,只是调整了CronSchedule.Builder 的调用 代码如下:
using DistributedLock;
using Repository.Database;
using TaskService.Libraries;
namespace TaskService.Tasks
{
public class DemoTask : BackgroundService
{
private readonly IServiceProvider serviceProvider;
private readonly ILogger logger;
public DemoTask(IServiceProvider serviceProvider, ILogger<DemoTask> logger)
{
this.serviceProvider = serviceProvider;
this.logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
CronSchedule.Builder(this);
await Task.Delay(-1, stoppingToken);
}
[CronSchedule(Cron = "0/1 * * * * ?")]
public void ClearLog()
{
try
{
using var scope = serviceProvider.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
//省略业务代码
Console.WriteLine("ClearLog:" + DateTime.Now);
}
catch (Exception ex)
{
logger.LogError(ex, "DemoTask.ClearLog");
}
}
[CronSchedule(Cron = "0/5 * * * * ?")]
public void ClearCache()
{
try
{
using var scope = serviceProvider.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
var distLock = scope.ServiceProvider.GetRequiredService<IDistributedLock>();
//省略业务代码
Console.WriteLine("ClearCache:" + DateTime.Now);
}
catch (Exception ex)
{
logger.LogError(ex, "DemoTask.ClearCache");
}
}
}
}
然后启动我们的项目就可以看到如下的运行效果:
最上面连着两个 16:25:53 并不是重复调用了,只是因为这个任务配置的是 1秒钟执行1次,第一次启动任务的时候执行的较为耗时,导致第一次执行和第二次执行进入到方法中的时间差太短了,这个只在第一次产生,对后续的执行计划没有影响。
至此 .NET 纯原生实现 Cron 定时任务执行,未依赖第三方组件 (Timer 优化版) 就讲解完了,有任何不明白的,可以在文章下面评论或者私信我,欢迎大家积极的讨论交流,有兴趣的朋友可以关注我目前在维护的一个 .NET 基础框架项目,项目地址如下
https://github.com/berkerdong/NetEngine.git
https://gitee.com/berkerdong/NetEngine.git
看了大家的讨论之后我又对 CronSchedule.cs 做了如下三点调整:
- 添加了一个 HashTable 用于记录每个任务已经执行过的时间点,每次执行之前尝试给 HashTable 插入一个执行记录,如果插入成功没有触发异常,则认为没有执行过可以触发任务,这里主要是利用了 HashTable Key 不可重复的逻辑
- 同时把 Key 插入到了一个 List 中,每次 mainTimer 执行的时候先检索 5秒钟以前的 key 把他们从 HashTable 中移除
- mainTimer 的执行间隔我调整为了900ms,根据微软的说法,在现行的 Windows 系统中 Timer 的分辨率为 15ms,所以 900ms 间隔扫描一次是否有需要执行的任务,同时利用了上面的逻辑避免了任务的重复调度。
using Common;
using System.Collections;
using System.Reflection;
namespace TaskService.Libraries
{
public class CronSchedule
{
private static readonly List<ScheduleInfo> scheduleList = new();
private static Timer mainTimer;
private static readonly Hashtable historyList = new();
private static readonly List<string> historyKeyList = new();
public static void Builder(object context)
{
var taskList = context.GetType().GetMethods().Where(t => t.GetCustomAttributes(typeof(CronScheduleAttribute), false).Length > 0).ToList();
foreach (var action in taskList)
{
string cron = action.CustomAttributes.Where(t => t.AttributeType == typeof(CronScheduleAttribute)).FirstOrDefault()!.NamedArguments.Where(t => t.MemberName == "Cron" && t.TypedValue.Value != null).Select(t => t.TypedValue.Value!.ToString()).FirstOrDefault()!;
scheduleList.Add(new ScheduleInfo
{
CronExpression = cron,
Action = action,
Context = context
});
}
if (mainTimer == default)
{
mainTimer = new(Run, null, 0, 900);
}
}
private static void Run(object? state)
{
var nowTime = DateTime.Parse(DateTimeOffset.UtcNow.ToString("yyyy-MM-dd HH:mm:ss"));
foreach (var key in historyKeyList)
{
var keyTime = DateTime.Parse(key[..19]);
if (keyTime! <= nowTime.AddSeconds(-5))
{
historyList.Remove(key);
}
}
foreach (var item in scheduleList)
{
if (item.LastTime != null)
{
var nextTime = DateTime.Parse(CronHelper.GetNextOccurrence(item.CronExpression, item.LastTime.Value).ToString("yyyy-MM-dd HH:mm:ss"));
if (nextTime == nowTime)
{
try
{
string key = nextTime.ToString("yyyy-MM-dd HH:mm:ss") + " " + item.Action.DeclaringType?.FullName + "." + item.Action.Name;
historyList.Add(key, null);
historyKeyList.Add(key);
item.LastTime = DateTimeOffset.Now;
_ = Task.Run(() =>
{
item.Action.Invoke(item.Context, null);
});
}
catch
{
}
}
}
else
{
item.LastTime = DateTimeOffset.Now.AddSeconds(5);
}
}
}
private class ScheduleInfo
{
public string CronExpression { get; set; }
public MethodInfo Action { get; set; }
public object Context { get; set; }
public DateTimeOffset? LastTime { get; set; }
}
}
}
本模块主要是用于日常普通项目中的一些任务定时调度需求。
相关文章
- 用.NET开发的磁力搜索引擎——btbook.net「建议收藏」
- ASP.NET Core 6框架揭秘实例演示[35]:利用Session保留语境
- 踩坑 Windows 服务来宿主 .NET 程序
- 【愚公系列】2023年01月 .NET CORE工具案例-RedLock.net实现分布式锁
- 联手开发:.NET与Oracle的合作之路(.net和oracle)
- .NET 5.0 正式版发布:应用可在 ARM64 设备上原生运行
- Linux上安装.NET:提高开发效率,拓展技术栈(linux安装.net)
- Exploring the Benefits and Advantages of Using MongoDB for .NET Development(mongodbnet)
- Net开发Oracle数据库新技术攻关挑战(.net开发oracle)
- NET操作MySQL数据库快速入门(.net读写mysql)
- NET环境下MySQL数据库的使用实践(.net支持mysql吗)
- 使用NET来连接MySQL数据库的简单方法(.net怎么连mysql)
- NET备份MySQL提升数据安全性(.net 备份mysql)
- Net平台下MySQL数据库操作实践(.net mysql操作)
- 在ASP.NET里得到网站的域名
- ASP.NET中生成Excel遇到的问题及改进方法
- .NET中TextBox控件设置ReadOnly=true后台取不到值三种解决方法
- .NET中关于脏读不可重复读与幻读的代码示例
- ASP.NET中操作SQL数据库(连接字符串的配置及获取)
- ASP.NET设置404页面返回302HTTP状态码的解决方法
- c#利用system.net发送html格式邮件
- .Net基于MVC4WebApi输出Json格式实例
- asp.net发送邮件实现方法