Asp.Net Core 輕鬆學-基於微服務的後臺任務調度管理器

来源:https://www.cnblogs.com/viter/archive/2018/12/07/10078488.html
-Advertisement-
Play Games

在 Asp.Net Core 中,我們常常使用 System.Threading.Timer 這個定時器去做一些需要長期在後臺運行的任務,但是這個定時器在某些場合卻不太靈光,而且常常無法控制啟動和停止,我們需要一個穩定的,類似 WebHost 這樣主機級別的任務管理程式,但是又要比 WebHost ... ...


前言

    在 Asp.Net Core 中,我們常常使用 System.Threading.Timer 這個定時器去做一些需要長期在後臺運行的任務,但是這個定時器在某些場合卻不太靈光,而且常常無法控制啟動和停止,我們需要一個穩定的,類似 WebHost 這樣主機級別的任務管理程式,但是又要比 WebHost 要輕便。

    由此,我找到了官方推薦的 IHostedService 介面,該介面位於程式集 Microsoft.Extensions.Hosting.Abstractions 的 命名空間 Microsoft.Extensions.Hosting。該介面自 .Net Core 2.0 開始提供,按照官方的說法,由於該介面的出現,下麵的這些應用場景的代碼都可以刪除了。

歷史場景列表

  1. 輪詢資料庫以查找更改的後臺任務
  2. 從 Task.Run() 開始的後臺任務
  3. 定期更新某些緩存的計劃任務
  4. 允許任務在後臺線程上執行的 QueueBackgroundWorkItem 實現
  5. 在 Web 應用後臺處理消息隊列中的消息,同時共用 ILogger 等公共服務

1. 原理解釋

1.1 首先來看介面 IHostedService 的代碼,這需要花一點時間去理解它的原理,你也可以跳過本段直接進入第二段

namespace Microsoft.Extensions.Hosting
{
    //
    // Summary:
    //     Defines methods for objects that are managed by the host.
    public interface IHostedService
    {
        //
        // Summary:
        // Triggered when the application host is ready to start the service.
        Task StartAsync(CancellationToken cancellationToken);
        //
        // Summary:
        // Triggered when the application host is performing a graceful shutdown.
        Task StopAsync(CancellationToken cancellationToken);
    }
}

1.2 非常簡單,只有兩個方法,但是非常重要,這兩個方法分別用於程式啟動和退出的時候調用,這和 Timer 有著雲泥之別,這是質變。

1.3 從看到 IHostedService 這個介面開始,我就習慣性的想,按照微軟的慣例,某個介面必然有其預設實現的抽象類,然後我就看到了 Microsoft.Extensions.Hosting.BackgroundService ,果然,前人種樹後人乘涼,在 BackgroundService 類中,介面已經實現好了,我們只需要去實現 ExecuteAsync 方法

1.4 BackgroundService 內部代碼如下,值得註意的是 BackgroundService 從 .Net Core 2.1 開始提供,所以,使用舊版本的同學們可能需要升級一下

public abstract class BackgroundService : IHostedService, IDisposable
{
    private Task _executingTask;
    private readonly CancellationTokenSource _stoppingCts = 
                                                   new CancellationTokenSource();

    protected abstract Task ExecuteAsync(CancellationToken stoppingToken);

    public virtual Task StartAsync(CancellationToken cancellationToken)
    {
        // Store the task we're executing
        _executingTask = ExecuteAsync(_stoppingCts.Token);

        // If the task is completed then return it, 
        // this will bubble cancellation and failure to the caller
        if (_executingTask.IsCompleted)
        {
            return _executingTask;
        }

        // Otherwise it's running
        return Task.CompletedTask;
    }
    
    public virtual async Task StopAsync(CancellationToken cancellationToken)
    {
        // Stop called without start
        if (_executingTask == null)
        {
            return;
        }

        try
        {
            // Signal cancellation to the executing method
            _stoppingCts.Cancel();
        }
        finally
        {
            // Wait until the task completes or the stop token triggers
            await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite,
                                                          cancellationToken));
        }

    }

    public virtual void Dispose()
    {
        _stoppingCts.Cancel();
    }
}

1.5 BackgroundService 內部實現了 IHostedService 和 IDisposable 介面,從代碼實現可以看出,BackgroundService 充分實現了任務啟動註冊和退出清理的邏輯,並保證在任務進入 GC 的時候及時的退出,這很重要。

2. 開始使用

2.1 首先創一個通用的任務管理類 BackManagerService ,該類繼承自 BackgroundService

    public class BackManagerService : BackgroundService
    {
        BackManagerOptions options = new BackManagerOptions();
        public BackManagerService(Action<BackManagerOptions> options)
        {
            options.Invoke(this.options);
        }
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            // 延遲啟動
            await Task.Delay(this.options.CheckTime, stoppingToken);

            options.OnHandler(0, $"正在啟動托管服務 [{this.options.Name}]....");
            stoppingToken.Register(() =>
            {
                options.OnHandler(1, $"托管服務  [{this.options.Name}] 已經停止");
            });

            int count = 0;
            while (!stoppingToken.IsCancellationRequested)
            {
                count++;
                options.OnHandler(1, $" [{this.options.Name}] 第 {count} 次執行任務....");
                try
                {
                    options?.Callback();
                    if (count == 3)
                        throw new Exception("模擬業務報錯");
                }
                catch (Exception ex)
                {
                    options.OnHandler(2, $" [{this.options.Name}] 執行托管服務出錯", ex);
                }
                await Task.Delay(this.options.CheckTime, stoppingToken);
            }
        }

        public override Task StopAsync(CancellationToken cancellationToken)
        {
            options.OnHandler(3, $" [{this.options.Name}] 由於進程退出,正在執行清理工作");
            return base.StopAsync(cancellationToken);
        }
    }
  • BackManagerService 類繼承了 BackgroundService ,並實現了 ExecuteAsync(CancellationToken stoppingToken) 方法,在 ExecuteAsync 方法內,先是延遲啟動任務,接下來進行註冊和調度,這裡使用 while 迴圈判斷如果令牌沒有取消,則一直輪詢,而輪詢的關鍵在於下麵的代碼
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        ...
        while (!stoppingToken.IsCancellationRequested)
            {
                ...
                await Task.Delay(this.options.CheckTime, stoppingToken);
            }
    }

while 迴圈內部使用 Task.Delay 設置時間,在 this.options.CheckTime 計時結束後繼續下一輪的調度任務
實際上,Task.Delay 方法內部也是使用了 System.Threading.Timer 類進行計時,但是,當內部的 Timer 計時結束後,會馬上被 Dispose 掉

2.2 任務管理類 BackManagerService 包含一個帶參數的構造方法,是一個匿名委托,需要傳入參數 BackManagerOptions,該參數表示一個任務的調度參數

2.3 創建 BackManagerOptions 任務調度操作類

    public class BackManagerOptions
    {
        /// <summary>
        ///  任務名稱
        /// </summary>
        public string Name { get; set; }
        /// <summary>
        ///  獲取或者設置檢查時間間隔,單位:毫秒,預設 10 秒
        /// </summary>
        public int CheckTime { get; set; } = 10 * 1000;
        /// <summary>
        ///  回調委托
        /// </summary>
        public Action Callback { get; set; }
        /// <summary>
        ///  執行細節傳遞委托
        /// </summary>
        public Action<BackHandler> Handler { get; set; }

        /// <summary>
        ///  傳遞內部信息到外部組件中,以方便處理擴展業務
        /// </summary>
        /// <param name="level">0=Info,1=Debug,2=Error,3=exit</param>
        /// <param name="message"></param>
        /// <param name="ex"></param>
        /// <param name="state"></param>
        public void OnHandler(int level, string message, Exception ex = null, object state = null)
        {
            Handler?.Invoke(new BackHandler() { Level = level, Message = message, Exception = ex, State = state });
        }
    }

2.4 該 BackManagerOptions 任務調度操作類包含了一些基礎的設置內容,比如任務名稱,執行周期間隔,回調委托 Callback,任務管理器內部執行細節傳遞委托 Handler,這些定義非常有用,下麵會用到

2.5 其中,執行細節傳遞委托 Handler 包含一個參數,其實就是傳遞的細節,非常簡單的一個實體對象類,無非就是信息級別,消息描述,異常信息,執行對象

    public class BackHandler
    {
        /// <summary>
        ///  0=Info,1=Debug,2=Error
        /// </summary>
        public int Level { get; set; }
        public string Message { get; set; }
        public Exception Exception { get; set; }
        public object State { get; set; }
    }

2.6 定義好上面的 3 個對象後,現在來創建一個訂單管理類,用於定時輪詢資料庫訂單是否超時未付款,然後返還庫存

 public class OrderManagerService
    {
        public void CheckOrder()
        {
            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.WriteLine("==業務執行完成==");
            Console.ForegroundColor = ConsoleColor.Gray;
        }

        public void OnBackHandler(BackHandler handler)
        {
            switch (handler.Level)
            {
                default:
                case 0: break;
                case 1:
                case 3: Console.ForegroundColor = ConsoleColor.Yellow; break;
                case 2: Console.ForegroundColor = ConsoleColor.Red; break;
            }
            Console.WriteLine("{0} | {1} | {2} | {3}", handler.Level, handler.Message, handler.Exception, handler.State);
            Console.ForegroundColor = ConsoleColor.Gray;

            if (handler.Level == 2)
            {
                // 服務執行出錯,進行補償等工作
            }
            else if (handler.Level == 3)
            {
                // 退出事件,清理你的業務
                CleanUp();
            }
        }

        public void CleanUp()
        {
            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.WriteLine("==清理完成==");
            Console.ForegroundColor = ConsoleColor.Gray;
        }
    }

2.7 這個 OrderManagerService 業務類定義了 3 個方法,CheckOrder 檢查訂單,OnBackHandler 輸出執行信息,CleanUp 在程式退出的時候去做一些清理工作,非常簡單,前兩個方法是用於註冊到 BackManagerService 任務調度器中,後一個是內部方法。

3. 註冊 BackManagerService 任務調度器到進程中

3.1 定義好業務類後,我們需要把它註冊到進程中,以便程式啟動和退出的時候自動執行

3.2 在 Startup.cs 的 ConfigureServices 方法中註冊托管主機,看下麵的代碼

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

            services.AddSingleton<Microsoft.Extensions.Hosting.IHostedService, BackManagerService>(factory =>
            {
                OrderManagerService order = new OrderManagerService();
                return new BackManagerService(options =>
                 {
                     options.Name = "訂單超時檢查";
                     options.CheckTime = 5 * 1000;
                     options.Callback = order.CheckOrder;
                     options.Handler = order.OnBackHandler;
                 });
            });
        }

3.3 上面的代碼通過將 BackManagerService 註冊到托管主機中,併在初始化的時候設置了 BackManagerOptions ,然後將 OrderManagerService 的方法註冊到 BackManagerOptions 的委托中,實現業務執行

3.4 運行程式,觀察輸出結果

3.4 輸出結果清晰的表示創建的托管服務運行良好,我們來看一下執行順序

執行順序

  1. 啟動托管服務
  2. 執行“訂單超時檢查”任務,連續執行了 3 次,間隔 5 秒,每次執行都向外部傳遞了執行細節信息
  3. 由於我們故意設置任務執行到第 3 次的時候模擬拋出異常,可以看到,異常被正確的捕獲並安全的傳遞到外部
  4. 任務繼續執行
  5. 強制終止了程式,然後托管服務收到了程式停止的信號並立即進行了清理工作,通知外部業務委托執行清理
  6. 清理完成,托管服務停止並退出

3.5 註冊多個托管服務,通過定義的 BackManagerService 任務調度器,我們甚至具備了同時托管數個任務的能力,而我們只需要在 ConfigureServices 增加一行代碼

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

            services.AddSingleton<Microsoft.Extensions.Hosting.IHostedService, BackManagerService>(factory =>
            {
                OrderManagerService order = new OrderManagerService();
                return new BackManagerService(options =>
                 {
                     options.Name = "訂單超時檢查";
                     options.CheckTime = 5 * 1000;
                     options.Callback = order.CheckOrder;
                     options.Handler = order.OnBackHandler;
                 });
            });

            services.AddSingleton<Microsoft.Extensions.Hosting.IHostedService, BackManagerService>(factory =>
            {
                OrderManagerService order = new OrderManagerService();
                return new BackManagerService(options =>
                {
                    options.Name = "成交數量統計";
                    options.CheckTime = 2 * 1000;
                    options.Callback = order.CheckOrder;
                    options.Handler = order.OnBackHandler;
                });
            });
        }

3.6 為了方便,我們還是使用 OrderManagerService 來模擬業務,只是把任務名稱改成 "成交數量統計",並設置任務執行周期間隔為 2 秒

3.7 現在來運行程式,觀察輸出

3.8 輸出結果正常,兩個托管服務獨立運行,互不幹擾,藍色為 "成交數量統計",白色為 "訂單超時檢查"

結語

  • 得益於 .Net Core 提供的輕量型主機 IHostedService,我們可以方便的把後臺任務註冊到托管主機中,托管主機隨著宿主進程的啟動和退出執行相關的業務邏輯,這點非常重要,由於這種人性化的設計,我們可以在宿主進程啟動和退出的時候去做一些業務級別的工作。
  • 值得註意的是,IHostedService 中的方法 StartAsync 會在服務啟動的時候馬上執行,這可能導致宿主進程並未完全初始化業務數據,導致托管任務報錯,所以我們採用了延遲啟動,即在 StartAsync 內部使用代碼阻止任務立即執行
  protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            // 延遲啟動
            await Task.Delay(this.options.CheckTime, stoppingToken);
            ...
        }
  • 在預設情況下, CancellationToken 令牌取消的超時時間為 5 秒,如果你希望留更多的時間給業務處理,可以通過下麵的代碼修改,比如本示例設置為 15 秒後超時
        public static void Main(string[] args)
        {
            CreateWebHostBuilder(args)
                .UseShutdownTimeout(TimeSpan.FromSeconds(15))
                .Build().Run();
        }
  • 本次行文略顯羅嗦,代碼量也稍大了一些,主要是希望大家可以去理解原理後,使用起來心裡比較有底一些

示例代碼下載

https://files.cnblogs.com/files/viter/Ron.BackHost.zip


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

-Advertisement-
Play Games
更多相關文章
  • using System.Web.Routing; //重寫System.Web.Routing中Initialize方法 protected override void Initialize(RequestContext requestContext) { base.Initialize(requ... ...
  • 一、搭建項目 1、創建一個ASP.NET Core MVC 項目 2、nuget 下載和安裝 MicroSoft.AspNetCore.SignalR vs提示版本衝突 這時我們選擇低版本即可 二、SignalR配置 1、在model中創建一個類MyHub 代碼如下 public class MyH ...
  • delegate void del(); class MyClass1 { public event del eventcount;//創建事件併發布 public void Count() { for (int i = 0; i < 100; i++) { ... ...
  • public class NullToEmptyStringResolver : DefaultContractResolver { /// /// 創建屬性 /// /// 類型 /// 序列化成員 /// protected override IList Creat... ...
  • 這是我定義的實體類 對應的資料庫表 映射文件 數據訪問層寫的是插入語句 錯誤: 捕捉到 NHibernate.Exceptions.GenericADOException HResult=-2146232832 Message=could not insert: [DaYou.Yun.Entity. ...
  • 由於本人是Java入門的開發,在C#開發中遇到的問題,在此記錄一下: 1、client端的send方法不管發送出去沒發送出去,總是顯示發送出去。 查資料得知,send方法是將數據發送到緩存區,並不是直接發送到server。 2、connected 方法,總是顯示已連接上。 一直以為connected ...
  • 在測試中經常會遇到請求一些https的url,但又沒有本地證書,這時候可以用下麵的方法忽略警告 ...
  • 這段時間因公司業務需要.net開發且需要用到DevExpress控制項,我自己研究學習了一下,用的是visual studio(2013)和DevExpress(V14.1.4),VS2013的下載安裝就不說,直接進入正題。 DevExpress(V14.1.4)安裝、破解和漢化的程式下載鏈接 鏈接: ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...