ASP.NET Core – 在后台作业中使用作用域服务

huangapple go评论74阅读模式
英文:

ASP.NET Core - Using scoped service in background job

问题

我在我的应用程序中有一些后台任务,它们会在一定时间间隔内(从几分钟到几个小时)修改数据库。所有这些任务都有一个while循环,其中使用stoppingToken.IsCancellationRequested条件。

目前我在循环内部创建和处理一个作用域,这意味着在每次迭代时都需要创建和处理一个作用域。我的问题是,从性能和安全性的角度考虑,我应该在哪里创建作用域?在循环内每次迭代时创建,还是在应用程序生命周期内的循环外部创建?

public class MyBackgroundJob : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;

    public MyBackgroundJob(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // 这里? 👇
        //using var scope = _serviceProvider.CreateScope();
        //var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();

        while (!stoppingToken.IsCancellationRequested)
        {
            // 还是这里? 👇
            using var scope = _serviceProvider.CreateScope();
            var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();

            // 进行一些操作

            try
            {
                await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
            }
            catch (TaskCanceledException)
            {
                // 应用程序正在关闭
                // 忽略此异常
            }
        }
    }
}
英文:

I have a few background jobs in my application that modify the database on an interval (from a few minutes to several hours). All of them have a while loop that uses the stoppingToken.IsCancellationRequested condition.

Currently I am creating and disposing a scope inside the loop, which means on every iteration, a scope needs to be created and disposed. My question is that from a performance and security point of view where should I create my scope? Inside the loop for each iteration or outside the loop once in the application lifetime?

public class MyBackgroundJob : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;

    public MyBackgroundJob(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // Here? 👇
        //using var scope = _serviceProvider.CreateScope();
        //var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();

        while (!stoppingToken.IsCancellationRequested)
        {
            // Or here? 👇
            using var scope = _serviceProvider.CreateScope();
            var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();

            // Do some work

            try
            {
                await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
            }
            catch (TaskCanceledException)
            {
                // application is shutting down
                // ignore this
            }
        }
    }
}

答案1

得分: 6

最好的做法可能是在循环内部创建范围。这取决于“执行某些工作”的方式以及如何使用“DbContext”。使用DbContext工厂而不是范围可能是一个更好的主意。


DbContext不是一个连接。它甚至不会一直保持连接处于打开状态,直到必须读取或保存数据。一旦完成,它就会关闭连接。DbContext是一个工作单元,用于跟踪所有加载的对象,对它们所做的任何更改,并在调用SaveChanges时将所有更改持久保存在单个事务中。将其处置也会丢弃更改,有效地将它们回滚。

如果循环执行单独的“事务”,那么范围和DbContext必须在循环内创建。这很可能是这里的情况,因为循环每5分钟重复一次。

使用长时间存在的DbContext没有真正的好处。要保持长时间存在的DbContext,您必须确保已禁用跟踪并且不使用Find(),以防止加载实体的缓存。从本质上讲,EF Core将像Dapper一样工作,执行查询并将结果映射到对象。

使用DbContext工厂

如果范围仅用于创建DbContext实例,一个替代方法是使用DbContextFactory。该工厂将用于在循环内部创建DbContext,而无需显式创建范围。

工厂被注册为Singleton,使用AddDbContextFactory

builder.Services.AddDbContextFactory<ApplicationDbContext>(
        options =>
            options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test"));

然后,后台服务将使用它来创建DbContext:

private readonly IDbContextFactory<ApplicationDbContext> _contextFactory;

public MyBackgroundJob(IDbContextFactory<ApplicationDbContext> contextFactory)
{
    _contextFactory = contextFactory;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    while (!stoppingToken.IsCancellationRequested)
    {
        using (var context = _contextFactory.CreateDbContext())
        {
            // 执行某些工作
        }
    }
    ...
}

使用池化的DbContext工厂

一般来说,DbContext对象很轻量级,但它们确实有一些开销。一个长时间存在的服务将在其生命周期内分配(和处置)许多这样的实例。为了甚至消除这种开销,可以使用池化的DbContext工厂来创建可重用的DbContext实例。当调用Dispose()时,DbContext实例会被清除并放入上下文池中,以便可以重复使用。

只需要更改工厂注册方式,使用AddDbContextPool

builder.Services.AddDbContextPool<ApplicationDbContext>(
        options =>
            options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test"));

服务代码保持不变。

英文:

Most likely, the scope should be created inside the loop. It depends on Do Some Work and how DbContext is used. A better idea may be to use a DbContext Factory instead of a scope.


A DbContext isn't a connection. It doesn't even keep a connection open until it has to read or save data. Once it's done, it closes the connection. A DbContext is a Unit-of-Work that tracks all loaded objects , any changes made to them and persists all changes in a single transaction when you call SaveChanges. Disposing it also discards the changes, effectively rolling them back.

If the loop performs individual "transactions", the scope and DbContext must be created inside the loop. This is most likely the case here, as the loop repeats every 5 minutes.

There's no real benefit to a long-lived DbContext. To keep a long-lived DbContext you'd have to ensure that tracking is disabled and Find() isn't used, to prevent caching of loaded entities. In essence, EF Core would work like Dapper, to execute queries and map results to objects.

Using a DbContext Factory

If the scope is only used to create DbContext instances, an alternative would be to use a DbContextFactory. The factory will be used to create the DbContext inside the loop without explicitly creating a scope.

The factory is registered as a Singleton with AddDbContextFactory:

builder.Services.AddDbContextFactory<ApplicationDbContext>(
        options =>
            options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test"));

The background service then uses it to create DbContexts:

private readonly IDbContextFactory<ApplicationDbContext> _contextFactory;

public MyBackgroundJob(IDbContextFactory<ApplicationDbContext> contextFactory)
{
    _contextFactory = contextFactory;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    while (!stoppingToken.IsCancellationRequested)
    {
        using (var context = _contextFactory.CreateDbContext())
        {
            // Do Some work
        }
    }
    ...

Using a pooled DbContext Factory

In general, DbContext objects are lightweight but they do have some overhead. A long lived service will allocate (and dispose) many of them over its lifetime though. To eliminate even this overhead, a pooled DbContext Factory can be used to create reusable DbContext instances. When Dispose() is called, the DbContext instance is cleared and put into a context pool so it can be reused.

Only the factory registration needs to change, to AddDbContextPool :

builder.Services.AddDbContextPool<ApplicationDbContext>(
        options =>
            options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test"));

The service code remains the same.

答案2

得分: 1

在每次迭代中内部循环内部还是在应用程序生命周期内外部循环?\n\n我的个人经验法则是为每个“tick”(即在这种情况下 - 无限循环的迭代)创建一个作用域,否则您可能会遇到不同的问题,例如资源/服务的生命周期被延长到应用程序的生命周期(虽然在某些应用程序中这可能没问题,比如CLI工具)。当在管道的某个地方使用EF Core上下文时,这可能特别明显,这可能导致不同的问题,如陈旧数据、性能下降和内存泄漏行为,这是由于更改跟踪引起的。

英文:

> Inside the loop for each iteration or outside the loop once in the application lifetime?

My personal rule of thumb is to create a scope per "tick" (i.e. in this case - iteration of the endless loop), otherwise you can encounter different problems with resource/service lifetime being extended to the lifetime of the app (though in some apps it can be fine for example CLI tools). This especially can be evident when working EF Core context used somewhere down the pipeline, which can lead do different problems like stale data, performance degradation and memory-leaky behavior due to the change tracking.

huangapple
  • 本文由 发表于 2023年3月31日 20:38:38
  • 转载请务必保留本文链接:https://go.coder-hub.com/75898623.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定