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会一直持有连接(或者长时间持有),这可能导致连接池中的连接被耗尽,因为连接不会被及时释放。
参考文献: