.NET 纯原生实现 Cron 定时任务执行,未依赖第三方组件 (Timer 优化版)
阅读原文时间:2023年07月09日阅读:6

在上个月写过一篇 .NET 纯原生实现 Cron 定时任务执行,未依赖第三方组件 的文章,当时 CronSchedule 的实现是使用了,每个服务都独立进入到一个 while 循环中,进行定期扫描是否到了执行时间来实现的,但是那个逻辑有些问题,经过各位朋友的测试,发现当多个任务的时候存在一定概率不按照计划执行的情况。

感谢各位朋友的积极淘汰,多交流一起进步。之前那个 while 循环的逻辑每循环一次 Task.Delay 1000 毫秒,无限循环,多个任务的时候还会同时有多个循环任务,确实不够好。

所以决定重构 CronSchedule 的实现,采用全局使用一个 Timer 的形式,每隔 1秒钟扫描一次任务队列看看是否有需要执行的任务,主要调整了 CronSchedule.cs

using Common;
using System.Reflection;

namespace TaskService.Libraries
{
public class CronSchedule
{
private static List 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

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器