Asp.Net Core 轻松学-基于微服务的后台任务调度管理器

,p>其他:Quartz.NET实现作业调度https://www.cnblogs.com/best/p/7658573.html

原文地址:https://www.cnblogs.com/viter/p/10078488.html

 

前言

    在 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();
<span class="hljs-function"><span class="hljs-keyword">protected</span> abstract Task <span class="hljs-title">ExecuteAsync</span><span class="hljs-params">(CancellationToken stoppingToken)</span></span>;

<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">virtual</span> Task <span class="hljs-title">StartAsync</span><span class="hljs-params">(CancellationToken cancellationToken)</span>
</span>{
    <span class="hljs-comment">// Store the task we're executing</span>
    _executingTask = ExecuteAsync(_stoppingCts.Token);

    <span class="hljs-comment">// If the task is completed then return it, </span>
    <span class="hljs-comment">// this will bubble cancellation and failure to the caller</span>
    <span class="hljs-keyword">if</span> (_executingTask.IsCompleted)
    {
        <span class="hljs-keyword">return</span> _executingTask;
    }

    <span class="hljs-comment">// Otherwise it's running</span>
    <span class="hljs-keyword">return</span> Task.CompletedTask;
}

<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">virtual</span> async Task <span class="hljs-title">StopAsync</span><span class="hljs-params">(CancellationToken cancellationToken)</span>
</span>{
    <span class="hljs-comment">// Stop called without start</span>
    <span class="hljs-keyword">if</span> (_executingTask == null)
    {
        <span class="hljs-keyword">return</span>;
    }

    <span class="hljs-keyword">try</span>
    {
        <span class="hljs-comment">// Signal cancellation to the executing method</span>
        _stoppingCts.Cancel();
    }
    finally
    {
        <span class="hljs-comment">// Wait until the task completes or the stop token triggers</span>
        await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite,
                                                      cancellationToken));
    }

}

<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">virtual</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Dispose</span><span class="hljs-params">()</span>
</span>{
    _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(<span class="hljs-number">0</span>, <span class="hljs-string">$"正在启动托管服务 [<span class="hljs-subst">{<span class="hljs-keyword">this</span>.options.Name}</span>]...."</span>);
        stoppingToken.Register(() =&gt;
        {
            options.OnHandler(<span class="hljs-number">1</span>, <span class="hljs-string">$"托管服务  [<span class="hljs-subst">{<span class="hljs-keyword">this</span>.options.Name}</span>] 已经停止"</span>);
        });

        <span class="hljs-keyword">int</span> count = <span class="hljs-number">0</span>;
        <span class="hljs-keyword">while</span> (!stoppingToken.IsCancellationRequested)
        {
            count++;
            options.OnHandler(<span class="hljs-number">1</span>, <span class="hljs-string">$" [<span class="hljs-subst">{<span class="hljs-keyword">this</span>.options.Name}</span>] 第 <span class="hljs-subst">{count}</span> 次执行任务...."</span>);
            <span class="hljs-keyword">try</span>
            {
                options?.Callback();
                <span class="hljs-keyword">if</span> (count == <span class="hljs-number">3</span>)
                    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> Exception(<span class="hljs-string">"模拟业务报错"</span>);
            }
            <span class="hljs-keyword">catch</span> (Exception ex)
            {
                options.OnHandler(<span class="hljs-number">2</span>, <span class="hljs-string">$" [<span class="hljs-subst">{<span class="hljs-keyword">this</span>.options.Name}</span>] 执行托管服务出错"</span>, ex);
            }
            <span class="hljs-keyword">await</span> Task.Delay(<span class="hljs-keyword">this</span>.options.CheckTime, stoppingToken);
        }
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">override</span> Task <span class="hljs-title">StopAsync</span>(<span class="hljs-params">CancellationToken cancellationToken</span>)
    </span>{
        options.OnHandler(<span class="hljs-number">3</span>, <span class="hljs-string">$" [<span class="hljs-subst">{<span class="hljs-keyword">this</span>.options.Name}</span>] 由于进程退出,正在执行清理工作"</span>);
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">base</span>.StopAsync(cancellationToken);
    }
}</code></pre>
  • 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; }
    <span class="hljs-comment"><span class="hljs-doctag">///</span> <span class="hljs-doctag">&lt;summary&gt;</span></span>
    <span class="hljs-comment"><span class="hljs-doctag">///</span>  传递内部信息到外部组件中,以方便处理扩展业务</span>
    <span class="hljs-comment"><span class="hljs-doctag">///</span> <span class="hljs-doctag">&lt;/summary&gt;</span></span>
    <span class="hljs-comment"><span class="hljs-doctag">///</span> <span class="hljs-doctag">&lt;param name="level"&gt;</span>0=Info,1=Debug,2=Error,3=exit<span class="hljs-doctag">&lt;/param&gt;</span></span>
    <span class="hljs-comment"><span class="hljs-doctag">///</span> <span class="hljs-doctag">&lt;param name="message"&gt;</span><span class="hljs-doctag">&lt;/param&gt;</span></span>
    <span class="hljs-comment"><span class="hljs-doctag">///</span> <span class="hljs-doctag">&lt;param name="ex"&gt;</span><span class="hljs-doctag">&lt;/param&gt;</span></span>
    <span class="hljs-comment"><span class="hljs-doctag">///</span> <span class="hljs-doctag">&lt;param name="state"&gt;</span><span class="hljs-doctag">&lt;/param&gt;</span></span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">OnHandler</span>(<span class="hljs-params"><span class="hljs-keyword">int</span> level, <span class="hljs-keyword">string</span> message, Exception ex = <span class="hljs-literal">null</span>, <span class="hljs-keyword">object</span> state = <span class="hljs-literal">null</span></span>)
    </span>{
        Handler?.Invoke(<span class="hljs-keyword">new</span> BackHandler() { Level = level, Message = message, Exception = ex, State = state });
    }
}</code></pre>

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;
        }
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">OnBackHandler</span><span class="hljs-params">(BackHandler handler)</span>
    </span>{
        <span class="hljs-keyword">switch</span> (handler.Level)
        {
            <span class="hljs-keyword">default</span>:
            <span class="hljs-keyword">case</span> <span class="hljs-number">0</span>: <span class="hljs-keyword">break</span>;
            <span class="hljs-keyword">case</span> <span class="hljs-number">1</span>:
            <span class="hljs-keyword">case</span> <span class="hljs-number">3</span>: Console.ForegroundColor = ConsoleColor.Yellow; <span class="hljs-keyword">break</span>;
            <span class="hljs-keyword">case</span> <span class="hljs-number">2</span>: Console.ForegroundColor = ConsoleColor.Red; <span class="hljs-keyword">break</span>;
        }
        Console.WriteLine(<span class="hljs-string">"{0} | {1} | {2} | {3}"</span>, handler.Level, handler.Message, handler.Exception, handler.State);
        Console.ForegroundColor = ConsoleColor.Gray;

        <span class="hljs-keyword">if</span> (handler.Level == <span class="hljs-number">2</span>)
        {
            <span class="hljs-comment">// 服务执行出错,进行补偿等工作</span>
        }
        <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (handler.Level == <span class="hljs-number">3</span>)
        {
            <span class="hljs-comment">// 退出事件,清理你的业务</span>
            CleanUp();
        }
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">CleanUp</span><span class="hljs-params">()</span>
    </span>{
        Console.ForegroundColor = ConsoleColor.Yellow;
        Console.WriteLine(<span class="hljs-string">"==清理完成=="</span>);
        Console.ForegroundColor = ConsoleColor.Gray;
    }
}</code></pre>

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&lt;Microsoft.Extensions.Hosting.IHostedService, BackManagerService&gt;(<span class="hljs-function"><span class="hljs-params">factory</span> =&gt;</span>
        {
            OrderManagerService order = <span class="hljs-keyword">new</span> OrderManagerService();
            <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> BackManagerService(<span class="hljs-function"><span class="hljs-params">options</span> =&gt;</span>
             {
                 options.Name = <span class="hljs-string">"订单超时检查"</span>;
                 options.CheckTime = <span class="hljs-number">5</span> * <span class="hljs-number">1000</span>;
                 options.Callback = order.CheckOrder;
                 options.Handler = order.OnBackHandler;
             });
        });
    }</code></pre>

3.3 上面的代码通过将 BackManagerService 注册到托管主机中,并在初始化的时候设置了 BackManagerOptions ,然后将 OrderManagerService 的方法注册到 BackManagerOptions 的委托中,实现业务执行

3.4 运行程序,观察输出结果

![在这里插入图片描述](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9pbWcyMDE4LmNuYmxvZ3MuY29tL2Jsb2cvMjY4ODIvMjAxODEyLzI2ODgyLTIwMTgxMjA2MTg0OTU0OTgxLTg3NTgwNzgyOC5wbmc)

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&lt;Microsoft.Extensions.Hosting.IHostedService, BackManagerService&gt;(<span class="hljs-function"><span class="hljs-params">factory</span> =&gt;</span>
        {
            OrderManagerService order = <span class="hljs-keyword">new</span> OrderManagerService();
            <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> BackManagerService(<span class="hljs-function"><span class="hljs-params">options</span> =&gt;</span>
             {
                 options.Name = <span class="hljs-string">"订单超时检查"</span>;
                 options.CheckTime = <span class="hljs-number">5</span> * <span class="hljs-number">1000</span>;
                 options.Callback = order.CheckOrder;
                 options.Handler = order.OnBackHandler;
             });
        });

        services.AddSingleton&lt;Microsoft.Extensions.Hosting.IHostedService, BackManagerService&gt;(<span class="hljs-function"><span class="hljs-params">factory</span> =&gt;</span>
        {
            OrderManagerService order = <span class="hljs-keyword">new</span> OrderManagerService();
            <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> BackManagerService(<span class="hljs-function"><span class="hljs-params">options</span> =&gt;</span>
            {
                options.Name = <span class="hljs-string">"成交数量统计"</span>;
                options.CheckTime = <span class="hljs-number">2</span> * <span class="hljs-number">1000</span>;
                options.Callback = order.CheckOrder;
                options.Handler = order.OnBackHandler;
            });
        });
    }

3.6 为了方便,我们还是使用 OrderManagerService 来模拟业务,只是把任务名称改成 "成交数量统计",并设置任务执行周期间隔为 2 秒

3.7 现在来运行程序,观察输出

![在这里插入图片描述](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9pbWcyMDE4LmNuYmxvZ3MuY29tL2Jsb2cvMjY4ODIvMjAxODEyLzI2ODgyLTIwMTgxMjA2MTg1MDA2NzgzLTY3ODUyMzIzNy5wbmc)

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://github.com/lianggx/EasyAspNetCoreDemo/tree/master/Ron.BackHost

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
将任务逻辑和任务调度彻底分离,并可通过Web界面远程监控和管理任务。 管理系统,作为一种高效的企业运营管理工具,旨在通过集成化、系统化的手段,对组织内部的各类资源进行规划、协调、控制和优化,以实现企业战略目标,提升运营效率,增强核心竞争力。以下是对管理系统的详细介绍: 一、定义与构成 管理系统是指由硬件设备、软件应用、数据资源、人员以及相关管理制度共同构建的,用于处理、监控、分析和决策各类业务活动的综合信息系统。它通常包括以下几个核心组成部分: 数据采集模块:负责从各类业务环节中实时、准确地收集信息,形成企业的基础数据资源。 数据分析模块:运用统计、人工智能等技术对数据进行深度挖掘和智能分析,提供决策支持。 业务流程管理模块:设计、执行、监控和优化业务流程,确保各项任务按照预定规则高效运转。 决策支持模块:基于数据分析结果,为管理者提供直观的可视化报告,辅助其进行科决策。 用户界面与交互模块:提供友好的人机交互界面,方便用户操作使用。 二、主要类型与功能 管理系统根据所针对的管理对象和领域,可分为多种类型,如: 人力资源管理系统(HRM):涵盖招聘、培训、绩效考核、薪酬福利等人力资源全流程管理,提升人才效能。 客户关系管理系统(CRM):集中管理客户信息,优化销售、营销和服务流程,提升客户满意度和忠诚度。 供应链管理系统(SCM):整合供应商、制造商、分销商、零售商等供应链各环节,实现物流、资金流、信息流的协同运作。 企业资源计划系统(ERP):对企业内部财务、生产、采购、库存、销售等各项资源进行全面集成管理,提高整体运营效率。 项目管理系统(PM):对项目全生命周期进行规划、跟踪、控制,确保项目按时、按质、按预算完成。 三、价值与优势 提高效率:自动化工作流程、标准化业务操作,显著减少人工干预,提升工作效率。 优化决策:实时数据分析与预测,提供精准的决策依据,助力管理层做出明智选择。 资源整合:打破部门壁垒,实现信息共享,优化资源配置,降低运营成本。 合规风控:内置法规遵循机制,强化内部控制,降低经营风险。 持续改进:通过对系统数据的持续监控与分析,驱动业务流程持续优化,促进企业创新与发展。 总的来说,管理系统作为现代企业管理的重要工具,以其强大的数据处理能力、智能化的决策支持和高效的业务流程管理,有力推动了企业的数字化转型,助力企业在日益激烈的市场竞争中保持竞争优势。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值