通过依赖关系注入 (DI) 提高 .NET 性能:实现高效的数据库、内存和连接处理

作者:微信公众号:【架构师老卢】
9-18 16:45
150

简介:为什么依赖注入不仅仅是代码结构

依赖关系注入 (DI) 在现代 .NET 开发中已成为必不可少的,它实现了松散耦合、可测试性和灵活性。但是,了解服务生命周期如何影响内存和资源连接至关重要。服务生命周期管理不善可能会导致内存泄漏、套接字耗尽和应用程序性能不佳等问题。

在本文中,我们将探讨内存和连接在 DI 中的作用,重点关注:

  • 不同 DI 生命周期的注意事项
  • 管理数据库连接HTTP 客户端内存使用情况的最佳实践。
  • 真实示例演示了避免常见陷阱的正确方法。

了解服务生命周期对内存和连接的影响

每个服务生命周期(、 和 )对内存和连接的影响不同。让我们用实际示例和最佳实践来分解每个生命周期。AddScopedAddTransientAddSingleton

1. : 管理每个请求的数据库连接AddScoped

AddScoped每个 HTTP 请求创建一次服务,并在请求结束时删除服务。这使得它非常适合数据库上下文 () 等服务,其中每个请求都需要自己的一组跟踪更改和单独的数据库连接。DbContext

Do:

  • 用于 或其他特定于请求的服务AddScopedDbContext
  • 在请求结束时释放资源以避免内存和连接泄漏。

Don’t:

  • 除非明确表示,否则不要在 或 类中使用服务,因为这可能会导致未定义的行为。ScopedSingletonTransient

示例:使用数据库服务AddScoped

在此示例中,我们有一个访问数据库的 a。该 已注册为 scoped,这意味着它将为每个请求创建一次,并在请求完成时被释放。ProductServiceAppDbContext

Startup.cs

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Register DbContext with Scoped lifetime (per request)
        services.AddDbContext<AppDbContext>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
        
        // Register repositories and Unit of Work with Scoped lifetime
        services.AddScoped<IProductRepository, ProductRepository>();
        services.AddScoped<IUnitOfWork, UnitOfWork>();
        // Register ProductService with Scoped lifetime
        services.AddScoped<IProductService, ProductService>();
    }
}

AppDbContext.cs

public class AppDbContext : DbContext  
{  
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)  
    {  
    }    public DbSet<Product> Products { get; set; }  
}

ProductService.cs

public interface IProductService
{
    Task<IEnumerable<Product>> GetTopProductsAsync(int count);
    Task AddProductAsync(Product product);
    Task UpdateProductAsync(Product product);
    Task DeleteProductAsync(int productId);
}

public class ProductService : IProductService
{
    private readonly IUnitOfWork _unitOfWork;

    public ProductService(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }

    public async Task<IEnumerable<Product>> GetTopProductsAsync(int count)
    {
        return await _unitOfWork.Products.GetTopProductsAsync(count);
    }

    public async Task AddProductAsync(Product product)
    {
        await _unitOfWork.Products.AddAsync(product);
        await _unitOfWork.CompleteAsync(); // Commit changes
    }

    public async Task UpdateProductAsync(Product product)
    {
        _unitOfWork.Products.Update(product);
        await _unitOfWork.CompleteAsync(); // Commit changes
    }

    public async Task DeleteProductAsync(int productId)
    {
        var product = await _unitOfWork.Products.GetByIdAsync(productId);
        if (product != null)
        {
            _unitOfWork.Products.Delete(product);
            await _unitOfWork.CompleteAsync(); // Commit changes
        }
    }
}

解释:

  • 为每个 HTTP 请求创建一个新的数据库,确保正确跟踪和保存数据库更改。AppDbContext
  • 内存得到有效管理,因为一旦请求完成,上下文就会被释放。

2. : 无状态、轻量级服务AddTransient

AddTransient每次请求服务时都会创建服务。这使得它们非常适合无状态的轻量级操作,例如在方法调用之间不保存任何状态的实用程序服务。

Do:

  • **用于 轻量级、无状态的服务,**如数据转换器或图像处理。AddTransient

Don’t:

  • 避免用于 维护连接的服务(例如,或 ),因为创建多个实例可能会导致性能问题。AddTransientDbContextHttpClient

示例:使用 Image OptimizerAddTransient

在此示例中,该服务是瞬态的,因为每个请求都独立处理图像,并且不需要调用之间的状态持久性。ImageOptimizer

Startup.cs

public class Startup  
{  
    public void ConfigureServices(IServiceCollection services)  
    {  
        // Register ImageOptimizer with Transient lifetime  
        services.AddTransient<IImageOptimizer, ImageOptimizer>();  
    }  
}

ImageOptimizer.cs

public interface IImageOptimizer
{
    byte[] OptimizeImage(byte[] imageData);
}
public class ImageOptimizer : IImageOptimizer
{
    public byte[] OptimizeImage(byte[] imageData)
    {
        // Simulate image optimization logic
        // Compress or resize the image
        return CompressImage(imageData);
    }
    private byte[] CompressImage(byte[] imageData)
    {
        // Mock compression logic
        return imageData;
    }
}

解释:

  • 每次请求服务时,都会创建一个新实例,该实例针对短期的无状态操作进行了优化。ImageOptimizer
  • 由于该服务是瞬态的,因此它可以避免不必要地保留内存或资源。

3. : 管理昂贵的长期资源AddSingleton

AddSingleton服务创建一次,并在整个应用程序中共享。单一实例服务最适合用于创建成本高昂且不需要经常更改的对象,例如 HTTP 客户端、缓存服务或配置提供程序。

Do:

  • 用于 应在应用程序生命周期内共享的服务,例如缓存服务或 HTTP 客户端。AddSingleton

Don’t:

  • 避免用于 有状态服务(如或特定于请求的服务),因为它们不是线程安全的,并且可能导致数据损坏或线程问题。SingletonDbContext

示例:在单一实例服务中使用IHttpClientFactory

在此示例中, 注册为 ,但它不是创建和管理自己的 ,而是用于创建正确管理的实例。WeatherServiceSingletonHttpClientIHttpClientFactoryHttpClient

Startup.cs

public class Startup  
{  
    public void ConfigureServices(IServiceCollection services)  
    {  
        // Register HttpClientFactory to manage HttpClient instances  
        services.AddHttpClient();  
  
        // Register WeatherService as Singleton  
        services.AddSingleton<IWeatherService, WeatherService>();  
    }  
}

WeatherService.cs

public interface IWeatherService
{
    Task<WeatherForecast> GetWeatherAsync(string city);
}
public class WeatherService : IWeatherService
{
    private readonly IHttpClientFactory _httpClientFactory;

    public WeatherService(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }

    public async Task<WeatherForecast> GetWeatherAsync(string city)
    {
        var client = _httpClientFactory.CreateClient();
        var response = await client.GetStringAsync($"https://api.weather.com/v3/wx/forecast?city={city}");
        return JsonConvert.DeserializeObject<WeatherForecast>(response);
    }
}

解释:

  • IHttpClientFactory 创建和管理实例。它处理连接池和 DNS 更改,使其成为手动管理长时间运行服务中的实例的更安全的替代方案。HttpClientHttpClient
  • 本身仍然是一个 ,确保服务逻辑可以在整个应用程序之间共享,但不会导致连接耗尽问题。WeatherServiceSingleton

何时将 Singleton 与其他服务一起使用

对于创建成本高昂且可以在多个请求中安全重用的服务,仍然是一个不错的选择。以下是一些替代示例:Singleton

  1. 缓存服务
public class MemoryCacheService : ICacheService
{
    private readonly IMemoryCache _memoryCache;

    public MemoryCacheService(IMemoryCache memoryCache)
    {
        _memoryCache = memoryCache;
    }

    public void SetCache<T>(string key, T item, TimeSpan duration)
    {
        _memoryCache.Set(key, item, duration);
    }

    public T GetCache<T>(string key)
    {
        _memoryCache.TryGetValue(key, out T value);
        return value;
    }
}

// Startup.cs
services.AddSingleton<ICacheService, MemoryCacheService>();

2. 配置提供程序

public class ConfigurationProvider : IConfigurationProvider
{
    private readonly IConfiguration _configuration;

    public ConfigurationProvider(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public string GetSetting(string key)
    {
        return _configuration[key];
    }
}

// Startup.cs
services.AddSingleton<IConfigurationProvider, ConfigurationProvider>();

关键要点

  • Do use 用于管理实例,而不是创建 .这可确保正确的连接处理并避免套接字耗尽。IHttpClientFactoryHttpClientHttpClientSingleton
  • 用于 创建成本高昂且可以在多个请求之间安全共享的服务,例如缓存服务、配置提供程序或日志记录服务。Singleton

依赖关系注入的顺序重要吗?

在 .NET 中,服务注册的顺序通常无关紧要,除非在某些情况下重写服务或使用条件逻辑。以下是需要考虑的关键因素:

1. 覆盖注册

如果多次注册同一服务,则上次注册将覆盖前一次注册。这对于将默认实现替换为自定义实现非常有用。

例:

services.AddSingleton<IExampleService, DefaultExampleService>();  
services.AddSingleton<IExampleService, CustomExampleService>(); // CustomExampleService will be used

2. 范围、瞬态和单例生命周期

不同服务生存期 (、 和 ) 的注册顺序不会影响其行为。服务的生命周期取决于它的注册方式,而不是顺序。ScopedTransientSingleton

services.AddSingleton<IExampleService, ExampleServiceSingleton>();  
services.AddScoped<IAnotherService, AnotherServiceScoped>();  
services.AddTransient<IOtherService, OtherServiceTransient>();

每个服务都根据其指定的生命周期运行,而不管注册顺序如何。

3. 有条件注册

当有条件地注册服务时(例如,基于环境),顺序很重要。满足条件的注册将优先。

if (env.IsDevelopment())  
{  
    services.AddScoped<IExampleService, DevelopmentExampleService>();  
}  
else  
{  
    services.AddScoped<IExampleService, ProductionExampleService>();  
}

4. 中间件顺序

虽然服务注册的顺序通常无关紧要,但方法中的中间件顺序很重要,因为中间件是按照添加的顺序执行的。Startup.Configure

app.UseAuthentication();  // Must come before UseAuthorization  
app.UseAuthorization();

DI 订单:

  • 服务注册顺序通常无关紧要,但多次注册同一服务时,最后一个注册优先。
  • 在配置中间件或覆盖服务时,顺序很重要。

结论:通过适当的 DI 寿命优化内存和连接

在 .NET 中有效使用依赖项注入需要的不仅仅是选择服务生存期。通过仔细管理内存和连接,可以防止内存泄漏和资源耗尽等常见问题。以下是关键要点:

Do:

  • 用于特定于请求的服务,例如 ,以确保适当的资源管理。AddScopedDbContext
  • 用于轻量级、无状态服务,例如图像处理器或实用程序服务。AddTransient
  • 用于创建成本高昂且可以共享的服务,例如 HTTP 客户端或缓存服务。AddSingleton

Don’t:

  • 避免用于有状态或特定于请求的服务,例如 HTTP 处理程序,因为这可能会导致内存和连接问题。SingletonDbContext
  • 不要在不考虑性能影响的情况下创建资源密集型服务的新实例。AddTransient

通过遵循这些最佳实践,您的 .NET 应用程序将针对内存管理和高效连接使用进行优化,从而确保更好的可扩展性和性能。

相关留言评论
昵称:
邮箱:
阅读排行