.NET 純原生實現 Cron 定時任務執行,未依賴第三方元件 (Timer 優化版)

2022-09-06 18:01:36

在上個月寫過一篇 .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 基礎框架專案,專案地址如下