2

.NET 纯原生实现 Cron 定时任务执行,未依赖第三方组件 (Timer 优化版) - 张晓栋

 1 year ago
source link: https://www.cnblogs.com/berkerdong/p/16662359.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

.NET 纯原生实现 Cron 定时任务执行,未依赖第三方组件 (Timer 优化版)

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

1963085-20220906162002068-1614848448.png

感谢各位朋友的积极探讨,多交流一起进步。之前那个 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");
            }
        }

    }
}

然后启动我们的项目就可以看到如下的运行效果:

1963085-20220906162641779-1848868114.png

最上面连着两个 16:25:53 并不是重复调用了,只是因为这个任务配置的是 1秒钟执行1次,第一次启动任务的时候执行的较为耗时,导致第一次执行和第二次执行进入到方法中的时间差太短了,这个只在第一次产生,对后续的执行计划没有影响。

至此 .NET 纯原生实现 Cron 定时任务执行,未依赖第三方组件 (Timer 优化版) 就讲解完了,有任何不明白的,可以在文章下面评论或者私信我,欢迎大家积极的讨论交流,有兴趣的朋友可以关注我目前在维护的一个 .NET 基础框架项目,项目地址如下

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK