asp.net core 系列 4 注入服务的生存期

  在容器中每个注册的服务,根据程序应用需求都可以选择合适的服务生存期,ASP.NET Core 服务有三种生存期配置Transient、Scoped、Singleton。

一.Transient

  --Transient:瞬间生命周期,在每次请求时会被创建一个新的实例。

  --适用场景:于无状态的服务,每次使用都需要新的实例。

  --在Web API中,如果一个服务是无状态的,并且每次请求都需要独立的实例,那么使用Transient是合适的。

  --在后台服务中,如果每次调用服务的方法时都需要新的实例,也可以使用Transient。

  --如下代码:使用Transient注入IOperationTransient,多次解析ScopedOperation 对象时,区别如下表格。

public class OperationController:ControllerBase
{    
    public IOperationTransient TransientOperation { get; }    
    public OperationController(IOperationTransient transientOperation){
            TransientOperation = transientOperation;
    }
      // 在同一个请求内,多次解析不是同一个对象
        [HttpGet("singlerequest")]
        public IActionResult SingleRequest()
        {
            var obj1 = TransientOperation;
            var obj2 = TransientOperation;  // 与 obj1 相同
            
            // 或者通过 ServiceProvider 再次解析
            var obj3 = HttpContext.RequestServices.GetService<IOperationTransient>();  // 与 obj1、ojb2 不相同
            return Ok(new {
                Obj1Hash = obj1.GetHashCode(),
                Obj2Hash = obj2.GetHashCode(),  // 相同
                Obj3Hash = obj3.GetHashCode()   // 不相同
            });
        }
}
api名称 HTTP请求 obj1实例 obj2实例 obj3实例 与前一次请求的关系
singlerequest
第一次请求

内存地址(0x1000)

内存地址(0x1000) 内存地址(0x2000)  obj1==obj2,obj1!=obj3, obj2!=obj3
singlerequest
第二次请求 内存地址(0x3000) 内存地址(0x3000) 内存地址(0x4000)  obj1==obj2,obj1!=obj3, obj2!=obj3

 

二.Scoped

  --Scoped: 作用域生存期,只有在同一个 HTTP 请求内多次解析 Scoped 服务,才会得到相同实例

  --适用场景:在Web请求范围内需要共享同一个实例的服务,如数据库上下文(DbContext)。

  --在Web API中,每个HTTP请求会创建一个作用域,因此在一个请求内多个地方使用同一个服务实例,可以使用Scoped。

  --在后台服务中,通常没有默认的作用域

  --如下代码,使用Scoped注入了IOperationScoped,下面在同一个 HTTP请求内多次解析 ScopedOperation 服务时,区别如下表格。

public class OperationController:ControllerBase
{    
    public IOperationScoped ScopedOperation { get; }    
    public OperationController(IOperationScoped scopedOperation)
    {
            ScopedOperation = scopedOperation;
    }
        
      // 在同一个请求内,多次解析是相同对象
        [HttpGet("singlerequest")]
        public IActionResult SingleRequest()
        {
            var obj1 = ScopedOperation;
            var obj2 = ScopedOperation;  // 与 obj1 相同
            
            // 或者通过 ServiceProvider 再次解析
            var obj3 = HttpContext.RequestServices.GetService<IOperationScoped>();  // 也与 obj1 相同
            
            return Ok(new {
                Obj1Hash = obj1.GetHashCode(),
                Obj2Hash = obj2.GetHashCode(),  // 相同
                Obj3Hash = obj3.GetHashCode()   // 相同
            });
        }
}
api名称 HTTP请求 obj1实例 obj2实例 obj3实例 与前一次请求的关系
singlerequest
第一次请求

内存地址(0x1000)

内存地址(0x1000) 内存地址(0x1000)  
singlerequest
第二次请求 内存地址(0x2000) 内存地址(0x2000) 内存地址(0x2000) 与第一次请求实例不同

 

三.Singleton

  Singleton: 单例生存期,在整个应用程序生命周期内只创建一个实例。

  --适用场景:全局共享的无状态服务,或者需要长时间保持状态的服务(如缓存)  

  --在它们第一次被请求时创建。每个后续请求将使用相同的实例。如果应用程序需要单例行为,建议让服务容器管理服务的生命周期,而不是在自己的类中实现单例模式。

  ----如下代码,使用Singleton注入了IOperationScoped,下面在同一个 HTTP请求内多次解析SingletonOperation 服务时,区别如下表格。  

public class OperationController:ControllerBase
{    
    public IOperationSingleton SingletonOperation { get; }    
    public OperationController(IOperationSingleton singletonOperation)
    {
            SingletonOperation = singletonOperation;
    }
        
      // 在多次请求内,多次解析是同一个对象
        [HttpGet("singlerequest")]
        public IActionResult SingleRequest()
        {
            var obj1 = SingletonOperation;
            var obj2 = SingletonOperation;  // 与 obj1 相同
            
            // 或者通过 ServiceProvider 再次解析
            var obj3 = HttpContext.RequestServices.GetService<IOperationSingleton>();  // 与 obj1、ojb2 相同
            
            return Ok(new {
                Obj1Hash = obj1.GetHashCode(),
                Obj2Hash = obj2.GetHashCode(),  // 相同
                Obj3Hash = obj3.GetHashCode()   // 相同
            });
        }
}
api名称 HTTP请求 obj1实例 obj2实例 obj3实例 与前一次请求的关系
singlerequest
第一次请求

内存地址(0x1000)

内存地址(0x1000) 内存地址(0x1000)  
singlerequest
第二次请求 内存地址(0x1000) 内存地址(0x1000) 内存地址(0x1000) 与第一次请求实例相同

  

 四.其它

  1) 在asp.net core中EF的DbContext通常被注册为Scoped生命周期,而不是Singleton,这是因为DbContext不是线程安全的,而Singleton实例会在多个请求之间共享,导致多个线程同时操作同一个DbContext实例,从而引发并发问题。还有数据一致性问题(工作单元Unit of Work)、资源管理(连接池耗尽)等问题。

  2)在ASP.NET Core中,对于中间件如Redis、Elasticsearch、消息队列等,通常使用Singleton生命周期进行注册,因为这些通常是无状态的或者本身是线程安全的,具体使用哪种生命周期取决于客户端的实现,也可以参考官方文档

    --对于Redis:StackExchange.Redis中的ConnectionMultiplexer是线程安全的,设计为Singleton。

    --对于Elasticsearch:NEST客户端(ElasticClient)是线程安全的,通常注册为Singleton。

    --对于消息队列:例如RabbitMQ,如果使用一个连接工厂,那么连接工厂通常是线程安全的,可以注册为Singleton。但是,具体的通道(Channel)可能不是线程安全的,需要根据情况而定。

  3)对于业务逻辑层(如应用服务、领域服务等)和数据库上下文(DbContext),通常使用Scoped生命周期

  4)Transient服务每次请求都会创建一个新实例。如果服务创建的成本很高(例如,初始化开销大),那么频繁创建可能会导致性能问题。另外,如果服务是无状态的,且创建开销小,那么使用Transient也是可以的   

  5)数据库上下文DbContext,使用Scoped会不会导致性能问题,因为每次http请求都会使用数据库打开连接,关闭连接,是不是没有使用到tcp的多路复用功能?

    答:连接池是由ADO.NET提供的,它是基于连接字符串的,每个不同的连接字符串会有一个连接池。而DbContext本身并不拥有连接池,它使用底层的ADO.NET连接,这些连接由连接池管理。 

  6)连接池是基于连接字符串ado.net的,EF的Scoped与Singleton注入的都是使用一个连接池。虽然连接池本身是共享的,但是DbContext实例会管理数据库连接。在Scoped生命周期中,DbContext会在请求结束时被释放,从而释放数据库连接回连接池。而Singleton的DbContext会一直持有连接(或者长时间持有),这可能导致连接池中的连接被耗尽,因为连接不会被及时释放。    

  参考文献:

    官方文档:ASP.NET Core 

 

posted on 2019-01-08 10:47  花阴偷移  阅读(2461)  评论(8)    收藏  举报

导航