.net core 實現基於 cron 表達式的任務調度

来源:https://www.cnblogs.com/weihanli/archive/2019/08/04/implement-job-schedule-via-cron-for-dotnetcore.html
-Advertisement-
Play Games

.net core 實現基於 cron 表達式的任務調度 Intro 上次我們實現了一個簡單的基於 Timer 的定時任務,詳細信息可以看 "這篇文章 " 。 但是使用過程中慢慢發現這種方式可能並不太合適,有些任務可能只希望在某個時間段內執行,只使用 timer 就顯得不是那麼靈活了,希望可以像 q ...


.net core 實現基於 cron 表達式的任務調度

Intro

上次我們實現了一個簡單的基於 Timer 的定時任務,詳細信息可以看這篇文章

但是使用過程中慢慢發現這種方式可能並不太合適,有些任務可能只希望在某個時間段內執行,只使用 timer 就顯得不是那麼靈活了,希望可以像 quartz 那樣指定一個 cron 表達式來指定任務的執行時間。

cron 表達式介紹

cron 常見於Unix類Unix操作系統之中,用於設置周期性被執行的指令。該命令從標準輸入設備讀取指令,並將其存放於“crontab”文件中,以供之後讀取和執行。該詞來源於希臘語 chronos(χρόνος),原意是時間。

通常,crontab儲存的指令被守護進程激活,crond 常常在後臺運行,每一分鐘檢查是否有預定的作業需要執行。這類作業一般稱為cron jobs

cron 可以比較準確的描述周期性執行任務的執行時間,標準的 cron 表達式是五位:

30 4 * * ? 五個位置上的值分別對應 分鐘/小時/日期/月份/周(day of week)

現在有一些擴展,有6位的,也有7位的,6位的表達式第一個對應的是秒,7個的第一個對應是秒,最後一個對應的是年份

0 0 12 * * ? 每天中午12點
0 15 10 ? * * 每天 10:15
0 15 10 * * ? 每天 10:15
30 15 10 * * ? * 每天 10:15:30
0 15 10 * * ? 2005 2005年每天 10:15

詳細信息可以參考:http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html

.NET Core CRON service

CRON 解析庫 使用的是 https://github.com/HangfireIO/Cronos
,支持五位/六位,暫不支持年份的解析(7位)

基於 BackgroundService 的 CRON 定時服務,實現如下:

public abstract class CronScheduleServiceBase : BackgroundService
{
        /// <summary>
        /// job cron trigger expression
        /// refer to: http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html
        /// </summary>
        public abstract string CronExpression { get; }

        protected abstract bool ConcurrentAllowed { get; }

        protected readonly ILogger Logger;

        private readonly string JobClientsCache = "JobClientsHash";

        protected CronScheduleServiceBase(ILogger logger)
        {
            Logger = logger;
        }

        protected abstract Task ProcessAsync(CancellationToken cancellationToken);

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            {
                var next = CronHelper.GetNextOccurrence(CronExpression);
                while (!stoppingToken.IsCancellationRequested && next.HasValue)
                {
                    var now = DateTimeOffset.UtcNow;

                    if (now >= next)
                    {
                        if (ConcurrentAllowed)
                        {
                            _ = ProcessAsync(stoppingToken);
                            next = CronHelper.GetNextOccurrence(CronExpression);
                            if (next.HasValue)
                            {
                                Logger.LogInformation("Next at {next}", next);
                            }
                        }
                        else
                        {
                            var machineName = RedisManager.HashClient.GetOrSet(JobClientsCache, GetType().FullName, () => Environment.MachineName); // try get job master
                            if (machineName == Environment.MachineName) // IsMaster
                            {
                                using (var locker = RedisManager.GetRedLockClient($"{GetType().FullName}_cronService"))
                                {
                                    // redis 互斥鎖
                                    if (await locker.TryLockAsync())
                                    {
                                        // 執行 job
                                        await ProcessAsync(stoppingToken);

                                        next = CronHelper.GetNextOccurrence(CronExpression);
                                        if (next.HasValue)
                                        {
                                            Logger.LogInformation("Next at {next}", next);
                                            await Task.Delay(next.Value - DateTimeOffset.UtcNow, stoppingToken);
                                        }
                                    }
                                    else
                                    {
                                        Logger.LogInformation($"failed to acquire lock");
                                    }
                                }
                            }
                        }
                    }
                    else
                    {
                        // needed for graceful shutdown for some reason.
                        // 1000ms so it doesn't affect calculating the next
                        // cron occurence (lowest possible: every second)
                        await Task.Delay(1000, stoppingToken);
                    }
                }
            }
        }

        public override Task StopAsync(CancellationToken cancellationToken)
        {
            RedisManager.HashClient.Remove(JobClientsCache, GetType().FullName); // unregister from jobClients
            return base.StopAsync(cancellationToken);
        }
    }

因為網站部署在多台機器上,所以為了防止併發執行,使用 redis 做了一些事情,Job執行的時候嘗試獲取 redis 中 job 對應的 master 的 hostname,沒有的話就設置為當前機器的 hostname,在 job 停止的時候也就是應用停止的時候,刪除 redis 中當前 job 對應的 master,job執行的時候判斷是否是 master 節點,是 master 才執行job,不是 master 則不執行。完整實現代碼:https://github.com/WeihanLi/ActivityReservation/blob/dev/ActivityReservation.Helper/Services/CronScheduleServiceBase.cs#L11

定時 Job 示例:

public class RemoveOverdueReservationService : CronScheduleServiceBase
{
    private readonly IServiceProvider _serviceProvider;
    private readonly IConfiguration _configuration;

    public RemoveOverdueReservationService(ILogger<RemoveOverdueReservationService> logger,
        IServiceProvider serviceProvider, IConfiguration configuration) : base(logger)
    {
        _serviceProvider = serviceProvider;
        _configuration = configuration;
    }

    public override string CronExpression => _configuration.GetAppSetting("RemoveOverdueReservationCron") ?? "0 0 18 * * ?";

    protected override bool ConcurrentAllowed => false;

    protected override async Task ProcessAsync(CancellationToken cancellationToken)
    {
        using (var scope = _serviceProvider.CreateScope())
        {
            var reservationRepo = scope.ServiceProvider.GetRequiredService<IEFRepository<ReservationDbContext, Reservation>>();
            await reservationRepo.DeleteAsync(reservation => reservation.ReservationStatus == 0 && (reservation.ReservationForDate < DateTime.Today.AddDays(-3)));
        }
    }
}

完整實現代碼:https://github.com/WeihanLi/ActivityReservation/blob/dev/ActivityReservation.Helper/Services/RemoveOverdueReservationService.cs

Memo

使用 redis 這種方式來決定 master 並不是特別可靠,正常結束的沒有什麼問題,最好還是用比較成熟的服務註冊發現框架比較好

Reference


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • [TOC] 原文鏈接: "Qt實現表格樹控制項 支持多級表頭" 一、概述 之前寫過一篇關於表格控制項多級表頭的文章,喜歡的話可以參考 "Qt實現表格控制項 支持多級列表頭、多級行表頭、單元格合併、字體設置等" 。今天這篇文章帶來了比表格更加複雜的控制項 樹控制項多級表頭實現。 在Qt中,表格控制項包含有水平和垂 ...
  • 例:抓取PhotoShop視頻教程 網址http://www.mxiaobei.com/?id=424 BeautifulSoup: https://beautifulsoup.readthedocs.io/zh_CN/v4.4.0/ Requests: http://cn.python reque ...
  • @EnableAutoConfiguration 原理分析 @SpringBootApplication中包含了@EnableAutoConfiguration註解,@EnableAutoConfiguration的作用是啟用Spring的自動載入配置。 SpringBoot一個最核心的觀點就是,約 ...
  • 上一節我們使用了Ribbon(基於 )進行微服務的調用,Ribbon的調用比較簡單,通過Ribbon組件對請求的服務進行攔截,通過 獲取到服務實例的 ,然後再去調用API。本節課我們使用更簡單的方式來實現,使用聲明式的 服務客戶端 ,我們只需要使用Feign來聲明介面,利用 來進行配置就可以使用了, ...
  • 請求頭常見參數 常見響應狀態碼 1.200:請求正常,伺服器正常的返回數據 2.301:永久重定問。比加在訪問w.jangdong.com的時候會重定向到w.Jd.cms 3.302:臨時重定向比如在訪問一個需要置錄的頁面的時候,而此時沒有登錄,那麼就會重定問到登錄頁面 4.400:請求的w1在服務 ...
  • 問題描述 體育老師小明要將自己班上的學生按順序排隊。他首先讓學生按學號從小到大的順序排成一排,學號小的排在前面,然後進行多次調整。一次調整小明可能讓一位同學出隊,向前或者向後移動一段距離後再插入隊列。 例如,下麵給出了一組移動的例子,例子中學生的人數為8人。 0)初始隊列中學生的學號依次為1, 2, ...
  • 本文主要講的就是java中的工廠設計模式中的三種模式,加強自己對其的理解。 ...
  • 最近公司做項目需要寫串口助手,於是從網上找教程著手寫了一下,基本的功能可以實現了,但是想要一個表盤的功能一直沒有找到教程,有些遺憾。大神們會的話給指導指導 謝謝啦 ! 下邊有源碼的連接,歡迎大家下載指導,有什麼問題也歡迎討論! 源碼下載鏈接:鏈接:https://pan.baidu.com/s/1M ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...