Asp.Net Core 轻松学-基于微服务的后台任务调度管理器
前言
在 Asp.Net Core 中,我们常常使用 System.Threading.Timer 这个定时器去做一些需要长期在后台运行的任务,但是这个定时器在某些场合却不太灵光,而且常常无法控制启动和停止,我们需要一个稳定的,类似 WebHost 这样主机级别的任务管理程序,但是又要比 WebHost 要轻便。
由此,我找到了官方推荐的 IHostedService 接口,该接口位于程序集 Microsoft.Extensions.Hosting.Abstractions 的 命名空间 Microsoft.Extensions.Hosting。该接口自 .Net Core 2.0 开始提供,按照官方的说法,由于该接口的出现,下面的这些应用场景的代码都可以删除了。
历史场景列表
- 轮询数据库以查找更改的后台任务
- 从 Task.Run() 开始的后台任务
- 定期更新某些缓存的计划任务
- 允许任务在后台线程上执行的 QueueBackgroundWorkItem 实现
- 在 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 输出结果清晰的表示创建的托管服务运行良好,我们来看一下执行顺序
执行顺序
- 启动托管服务
- 执行“订单超时检查”任务,连续执行了 3 次,间隔 5 秒,每次执行都向外部传递了执行细节信息
- 由于我们故意设置任务执行到第 3 次的时候模拟抛出异常,可以看到,异常被正确的捕获并安全的传递到外部
- 任务继续执行
- 强制终止了程序,然后托管服务收到了程序停止的信号并立即进行了清理工作,通知外部业务委托执行清理
- 清理完成,托管服务停止并退出
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://github.com/lianggx/EasyAspNetCoreDemo/tree/master/Ron.BackHost
Asp.Net Core 轻松学-基于微服务的后台任务调度管理器的更多相关文章
- Asp.Net Core 轻松学系列-1阅读指引目录
https://www.cnblogs.com/viter/p/10474091.html 目录 前言 1. 从安装到配置 2. 业务实现 3. 日志 4. 测试 5. 缓存使用 6.网络和通讯 7. ...
- Asp.Net Core 轻松学-多线程之Task(补充)
前言 在上一章 Asp.Net Core 轻松学-多线程之Task快速上手 文章中,介绍了使用Task的各种常用场景,但是感觉有部分内容还没有完善,在这里补充一下. 1. 任务的等待 在使用 ...
- WebAPI调用笔记 ASP.NET CORE 学习之自定义异常处理 MySQL数据库查询优化建议 .NET操作XML文件之泛型集合的序列化与反序列化 Asp.Net Core 轻松学-多线程之Task快速上手 Asp.Net Core 轻松学-多线程之Task(补充)
WebAPI调用笔记 前言 即时通信项目中初次调用OA接口遇到了一些问题,因为本人从业后几乎一直做CS端项目,一个简单的WebAPI调用居然浪费了不少时间,特此记录. 接口描述 首先说明一下,基于 ...
- 使用 ASP.NET Core 3.1 的微服务开发指南
使用 ASP.NET Core 3.1 的微服务 – 终极详细指南 https://procodeguide.com/programming/microservices-asp-net-core/ A ...
- Asp.Net Core 轻松学-使用MariaDB/MySql/PostgreSQL和支持多个上下文对象
前言 在上一篇文章中(Asp.Net Core 轻松学-10分钟使用EFCore连接MSSQL数据库)[https://www.cnblogs.com/viter/p/10243577.html],介 ...
- 如何从40亿整数中找到不存在的一个 webservice Asp.Net Core 轻松学-10分钟使用EFCore连接MSSQL数据库 WPF实战案例-打印 RabbitMQ与.net core(五) topic类型 与 headers类型 的Exchange
如何从40亿整数中找到不存在的一个 前言 给定一个最多包含40亿个随机排列的32位的顺序整数的顺序文件,找出一个不在文件中的32位整数.(在文件中至少确实一个这样的数-为什么?).在具有足够内存的情况 ...
- C# 中一些类关系的判定方法 C#中关于增强类功能的几种方式 Asp.Net Core 轻松学-多线程之取消令牌
1. IsAssignableFrom实例方法 判断一个类或者接口是否继承自另一个指定的类或者接口. public interface IAnimal { } public interface ID ...
- Asp.Net Core 轻松学-一行代码搞定文件上传 JSONHelper
Asp.Net Core 轻松学-一行代码搞定文件上传 前言 在 Web 应用程序开发过程中,总是无法避免涉及到文件上传,这次我们来聊一聊怎么去实现一个简单方便可复用文件上传功能:通过创建 ...
- Asp.Net Core 轻松学-利用文件监视进行快速测试开发
前言 在进行 Asp.Net Core 应用程序开发过程中,通常的做法是先把业务代码开发完成,然后建立单元测试,最后进入本地系统集成测试:在这个过程中,程序员的大部分时间几乎都花费在开发.运行 ...
随机推荐
- Java多线程:线程间通信之volatile与sychronized
由前文Java内存模型我们熟悉了Java的内存工作模式和线程间的交互规范,本篇从应用层面讲解Java线程间通信. Java为线程间通信提供了三个相关的关键字volatile, synchronized ...
- QString与string的相互转换
1.QString转换String string s = qstr.toStdString(); 2.String转换QString QString qstr2 = QString::fromStdS ...
- Tomcat PermGen space的解决方案
Tomcat报告 Caused by: java.lang.OutOfMemoryError: PermGen space异常 内存溢出PermGen space的全称是Permanent Gener ...
- linux基础-系统安装教程篇(centos6.5)
一.linux系统简介: Linux是一套免费使用和自由传播的类Unix操作系统,是一个基于POSIX和UNIX的多用户.多任务.支持多线程和多CPU的操作系统.它能运行主要的UNIX工具软件.应用程 ...
- Axios源码深度剖析 - 替代$.ajax,成为xhr的新霸主
前戏 在正式开始axios讲解前,让我们先想想,如何对现有的$.ajax进行简单的封装,就可以直接使用原声Promise了? let axios = function(config){ return ...
- C++ Singleton (单例) 模式最优实现
参考:http://blog.yangyubo.com/2009/06/04/best-cpp-singleton-pattern/ 索引 静态化并不是单例 (Singleton) 模式 饿汉模式 懒 ...
- LoadRunner Mysql性能优化
原文:https://blog.csdn.net/u011910905/article/details/49863787 一.查询与索引优化分析 1.show命令: show engines; 显示存 ...
- DUBBO报错分析—1(连接zookeeper成功,调用方法无反应,不报错)
思路分析 调用方法时,最后调用执行的是mapper的sql语句,既然调用对应的方法无法获取返回值,多是sql错误,但是并未报sql错误,说明可能是与sql相关的数据源配置错误. 过程调试 经尝试,当直 ...
- selenium自动化测试资源整理(含所有版本chrome、chromedriver、firefox下载链接)
今天把手头有的一些关于selenium测试的资源整理了一下,分享出来. 1. 所有版本chrome下载 是不是很难找到老版本的chrome?博主收集了几个下载chrome老版本的网站,其中哪个下载的是 ...
- python之分析decode、encode、unicode编码转换
decode()方法使用注册编码的编解码器的字符串进行解码.它默认为默认的字符串编码.decode函数可以将一个普通字符串转换为unicode对象.decode是将普通字符串按照参数中的编码格式进行解 ...