目录
一、托管服务的基本使用
我们在进行系统开发的时候,有的代码是需要运行在后台的,比如服务器启动的时候在后台预先加载数据到缓存、每天凌晨3点把数据导出到备份数据库、每隔5s在两张表之间同步一次数据,这些代码不是运行在前台的,因此不方便写到控制器中。ASP.NET Core中提供了托管服务来供我们编写运行在后台的代码。托管服务的使用非常简单,只要编写一个实现了IHostedService 接口的类即可。一般情况下我们编写从BackgroundService 类继承的类,因为 BackgroundService 实现了IHostedService接口,并且帮我们处理了任务的取消等逻辑。我们只要实现 BackgroundService类中定义的抽象方法ExecuteAsync,在 ExecuteAsync 方法中编写后台执行的代码即可。BackgroundService类实现了IDisposable接口,我们可以把任务结束后的清理代码写到 Dispose方法中。
二、编写一个托管服务
这个托管服务完成的任务是先延迟 5s,然后从“d:/1.xt”文件中读取内容,再延退20后把读到的文件内容输出到日志。因为程序中需要操作日志,所以我们在第4行代码中通过制造方法注入ILogger服务。ExecuteAsync 方法执行结束后,这个托管服务也就执行结束了,因此我们这里编写的托管服务不会常驻后台。(后面的案例也用这个代码)
public class DemoBgService : BackgroundService
{
private ILogger<DemoBgService> logger;
public DemoBgService(ILogger<DemoBgService> logger)
{
this.logger = logger;
}
protected override async Task ExecuteAsync (CancellationToken stoppingToken)
{
await Task.Delay(5000);
string s = await File.ReadAllTextAsync("d:/1.txt");
await Task.Delay(20000);
logger.LogInformation(s);
}
}
为了让托管服务能够运行,我们需要在 Program.cs 中调用AddHostedService 方法把它册到依赖注入容器中
services.AddHostedservice<DemoBgService>();
托管服务会随着应用程序启动,当然,托管服务是在后台运行的,不会阻塞 ASP.NET Core中其他程序的运行。
三、托管服务中使用依赖注入的陷阱
托管服务是以单例的生命周期注册到依赖注入容器中的。按照依赖注入容器的要求,长生命周期的服务不能依赖短生命周期的服务,因此我们可以在托管服务中通过构造方法注入其他生命周期为单例的服务,但是不能注入生命周期为范围或者瞬态的服务。由于日志系统的服务的生命周期为单例,因此我们在上面代码可以通过构造方法注入ILogger 服务,但是我们通过构造方法直接注入EF Core的上下文的话,程序就会抛出异常,因为通过AddDbContext注册的服务的生命周期是范围的。我们可以通过构造方法注入IServiceScopeFactory 服务,它可以用来创建IServiceScope对象,这样我们就可以通过IServiceScope 来创建短生命周期的服务了。
四、数据的定时导出
除了那些执行完任务就退出的托管服务之外,我们还可能需要编写常驻后台的托管服务比如监控消息队列,当有数据进入消息队列就处理。再如每隔 10s 把 A 数据库中的数据同步到B数据库中。因为 BackgroundService 的 ExecuteAsync 代码执行结束后托管服务就退出了,所以常驻后台的托管服务并不需要特殊的技术,我们只要让ExecuteAsync 中的代码一直执行不结束即可。
实现一个常驻后台的托管服务,它实现的功能是每隔 5s 对数据库中的数据进行汇总,然后把汇总结果写入一个文本文件。
public class ExplortStatisticBgService : BackgroundService
{
private readonly TestDbContext ctx;
private readonly ILogger<ExplortStatisticBgService> logger;
private readonly IServiceScope serviceScope;
public ExplortStatisticBgService(IServiceScopeFactory scopeFactory)
{
this.serviceScope = scopeFactory.CreateScope();
var sp = serviceScope.ServiceProvider;
this.ctx = sp.GetRequiredService<TestDbContext>();
this.logger = sp.GetRequiredservice<ILogger<ExplortStatisticBgService>>();
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await DoExecuteAsync();
await Task.Delay(5000);
}
catch (Exception ex)
{
logger.LogError(ex,"获取用户统计数据失败");
await Task.Delay(1000);
}
}
}
private async Task DoExecuteAsync()
{
var items = ctx.Users.GroupBy(u => u.CreationTime.Date).select (e => new { Date =
e.Key, Count = e.Count() }):
StringBuilder sb = new StringBuilder();
sb.AppendLine ($"Date:(DateTime.Now)");
foreach (var item in items)
{
sb.Append(item.Date) .AppendLine ($":(item.Count)");
}
await File.WriteAllTextAsync("d:/1.txt", sb.Tostring());
logger.LogInformation($"导出完成");
}
public override void Dispose()
{
base.Dispose();
serviceScope.Dispose();
}
}
通过构造方法注入 IServiceScopeFactory 服务,第 8 行中通过IServiceScopeFactory 的 CreateScope 方法创建 IServiceScope 对象,10-11行代码中我们就能获取 TestDbContext 服务的实例了。由于 IServiceScope 继承了 IDisposable接口,因此我们需要在托管服务的 Dispose 方法中销毁 serviceScope。13-28行代码的ExecuteAsync 方法中,通过 while 循环来实现反复执行 DoExecuteAsyne 方法;20行代码中,通过 Delay 来实现每次执行间隔 5s;如果 DoExecuteAsync 中出现异常,那么导致异常的问题可能会持续一段时间,因此 25 行代码中实现了每次异常发生之后暂停 1s再尝试执行,以避免频繁重试造成的CPU 占用率飙升。在29-41行代码中的DoExecuteAsync方法通过日期对数据进行分组统计,获取每一天注册的用户数量,然后把统计数据写入文本文件。