前言

无论是在我个人博客还是著作中,对于上下文实例池都只是通过大量文字描述来讲解其基本原理,而且也是浅尝辄止,导致我们对其认识仍是一知半解,本文我们摆源码,从源头开始分析。希望通过本文从源码的分析,我们大家都能了解到上注入下文和上下文实例池的区别在哪里,什么时候用上下文,什么时候用上下文实例池

上下文实例池原理准备工作

上下文实例池和线程池原理从概念来上讲一样,都是可重用,但在原理实现上却有本质区别。EF Core定义上下文实例池接口即IDbContextPool,将其接口实现抽象为:租赁(Rent)和归还(Return)。如下:

public interface IDbContextPool
{
DbContext Rent();

bool Return([NotNull] DbContext context);
}

那么租赁和归还的机制是什么呢?接下来我们从注入上下文实例池开始讲解。当我们在Startup中注入上下文和上下文实例池时,其他参数配置我们暂且忽略,从使用上二者最大区别在于,上下文可自定义设置生命周期,默认为Scope,而上下文实例池可自定义最大池大小,默认为128。那么问题来了,上下文实例池所管理的上下文的生命周期到底是什么呢?我们一探源码究竟,参数细节判断部分这里忽略分析

private static void CheckContextConstructors<TContext>()
where TContext : DbContext
{
var declaredConstructors = typeof(TContext).GetTypeInfo().DeclaredConstructors.ToList();
if (declaredConstructors.Count == 1
&& declaredConstructors[0].GetParameters().Length == 0)
{
throw new ArgumentException(CoreStrings.DbContextMissingConstructor(typeof(TContext).ShortDisplayName()));
}
}

首先判断上下文必须有构造函数,因存在隐式默认无参构造函数,所以继续增强判断,构造函数参数不能为0,否则抛出异常

AddCoreServices<TContextImplementation>(
serviceCollection,
(sp, ob) =>
{
optionsAction(sp, ob); var extension = (ob.Options.FindExtension<CoreOptionsExtension>() ?? new CoreOptionsExtension())
.WithMaxPoolSize(poolSize);

((IDbContextOptionsBuilderInfrastructure)ob).AddOrUpdateExtension(extension);
},ServiceLifetime.Singleton );

其次,以单例形式注入DbContextOptions,因每个上下文无论实例化多少次,其DbContextOptions不会发生改变

serviceCollection.TryAddSingleton(
​ sp => new DbContextPool<TContextImplementation>(
sp.GetService<DbContextOptions<TContextImplementation>>()));

然后,以单例形式注入上下文实例池接口实现,因为该实例中存在队列机制来维护上下文,所有此类必然为单例,同时,该实例需要用到DbContextOptions,所以提前注入DbContextOptions

serviceCollection.AddScoped<DbContextPool<TContextImplementation>.Lease>();

紧接着,以生命周期为Scope注入Lease类,此类作为上下文实例池嵌套密封类存在,从单词理解就是对上下文进行释放(归还)处理(接下来会讲到)

serviceCollection.AddScoped(
sp => (TContextService)sp.GetService<DbContextPool<TContextImplementation>.Lease>().Context);

最后,这里就是上下文实例池所管理的上下文,其生命周期为Scope,不可更改

上下文实例池原理构造实现

首先给出上下文实例池中重要属性,以免越往下看一脸懵

private const int DefaultPoolSize = 32;

private readonly ConcurrentQueue<TContext> _pool = new ConcurrentQueue<TContext>();

private readonly Func<TContext> _activator;

private int _maxSize;

private int _count;

private DbContextPoolConfigurationSnapshot _configurationSnapshot;

上述是对于注入上下文实例池所做的准备工作,接下来我们则来到上下文实例池具体实现

public DbContextPool([NotNull] DbContextOptions options)
{
_maxSize = options.FindExtension<CoreOptionsExtension>()?.MaxPoolSize ?? DefaultPoolSize; options.Freeze(); _activator = CreateActivator(options); if (_activator == null)
{
throw new InvalidOperationException(
CoreStrings.PoolingContextCtorError(typeof(TContext).ShortDisplayName()));
}
}

在其构造中,获取自定义实例池最大大小,若未设置则以DefaultPoolSize为准,DefaultPoolSize定义为常量32,然后,防止实例化上下文后DbContextOptions配置发生更改,此时调用Freeze方法进行冻结,接下来则是实例化上下文,此时将其包裹在委托中,还未真正实例化,继续看上述CreateActivator方法实现。

private static Func<TContext> CreateActivator(DbContextOptions options)
{
var constructors
= typeof(TContext).GetTypeInfo().DeclaredConstructors
.Where(c => !c.IsStatic && c.IsPublic)
.ToArray(); if (constructors.Length == 1)
{
var parameters = constructors[0].GetParameters(); if (parameters.Length == 1
&& (parameters[0].ParameterType == typeof(DbContextOptions)
|| parameters[0].ParameterType == typeof(DbContextOptions<TContext>)))
{
return
Expression.Lambda<Func<TContext>>(
Expression.New(constructors[0], Expression.Constant(options)))
.Compile();
}
} return null;
}

简言之,上下文构造函数和参数有且只能有一个,而且参数必须类型必须是DbContextOptions,最后通过lambda表达式构造上下文委托。通过上述分析,正常情况下,我们知道设计如此,上下文只能是显式有参构造,而且参数必须只能有一个且必须是DbContextOptions,但有些情况下,我们在上下文构造中确实需要使用注入实例,岂不玩不了,若存在这种需求,这里请参考之前文章(EntityFramework Core 3.x上下文构造函数可以注入实例呢?

上下文实例池原理本质实现

上下文实例池构造得到最大实例池大小以及构造上下文委托(并未真正使用),接下来则是对上下文进行租赁(Rent)和归还(Return)处理

public virtual TContext Rent()
{
if (_pool.TryDequeue(out var context))
{
Interlocked.Decrement(ref _count); ((IDbContextPoolable)context).Resurrect(_configurationSnapshot); return context;
} context = _activator(); ((IDbContextPoolable)context).SetPool(this); return context;
}

从上下文实例池中的队列去获取上下文,很显然,首次没有,于是就激活上下文委托,实例化上下文,若存在则将_count减1,然后将上下文的状态进行激活或复活处理。_count属性用来与获取到的实例池大小maxSize进行比较(至于如何比较,接下来归还用讲到),然后为防并发线程中断等机制,不能用简单的_count--,必须保持其原子性,所以用Interlocked,不清楚这个用法,补补基础。

public virtual bool Return([NotNull] TContext context)
{
if (Interlocked.Increment(ref _count) <= _maxSize)
{
((IDbContextPoolable)context).ResetState(); _pool.Enqueue(context); return true;
} Interlocked.Decrement(ref _count); return false;
}

当上下文释放时(释放时做什么处理,下面会讲),首先将上下文状态重置,无非就是将上下文所跟踪的模型(变更追踪机制)进行关闭处理等等,这里就不做深入探讨,接下来则是将上下文归还上下文到队列中。我们结合租赁和归还整体分析:设置池大小为32,若此时有33个请求,且处理时间较长,此时将直接租赁33个上下文,紧接着33个上下文陆续被释放,此时开始将0-31归还入队列,当索引为32时,此时_count为33,无法入队,怎么搞?此时将来到注入的Lease类释放处理

public TContext Context { get; private set; }

void IDisposable.Dispose()
{
if (_contextPool != null)
{
if (!_contextPool.Return(Context))
{
((IDbContextPoolable)Context).SetPool(null);
Context.Dispose();
} _contextPool = null;
Context = null;
}
}

若请求超出自定义池大小,且请求处理周期很长,那么在释放时,余下上下文则不能归还入队列,直接释放掉,同时上下文实例池将结束掉自身不再具备对该上下文的维护处理能力。我们再次回到租赁方法,当队列中存在可用的上下文时,可以知道每次都重新实例化一个上下文和上下文实例池管理上下文的本质区别在于对Resurrect方法的处理。

((IDbContextPoolable)context).Resurrect(_configurationSnapshot);

我们再来看看该方法具体处理情况怎样,是否存在什么魔法从而有所影响性能的地方,我们在指定场景必须使用实例池呢?

void IDbContextPoolable.Resurrect(DbContextPoolConfigurationSnapshot configurationSnapshot)
{
if (configurationSnapshot.AutoDetectChangesEnabled != null)
{
ChangeTracker.AutoDetectChangesEnabled = configurationSnapshot.AutoDetectChangesEnabled.Value;
ChangeTracker.QueryTrackingBehavior = configurationSnapshot.QueryTrackingBehavior.Value;
ChangeTracker.LazyLoadingEnabled = configurationSnapshot.LazyLoadingEnabled.Value;
ChangeTracker.CascadeDeleteTiming = configurationSnapshot.CascadeDeleteTiming.Value;
ChangeTracker.DeleteOrphansTiming = configurationSnapshot.DeleteOrphansTiming.Value;
}
else
{
((IResettableService)_changeTracker)?.ResetState();
} if (_database != null)
{
_database.AutoTransactionsEnabled
= configurationSnapshot.AutoTransactionsEnabled == null
|| configurationSnapshot.AutoTransactionsEnabled.Value;
}
}

哇,我们惊呆了,完全没啥,都不用我们再解释,只是简单设置变更追踪各个状态属性而已。毫无疑问,上下文实例确实可以重用上下文实例,若存在复杂的业务逻辑和吞吐量比较大的情况,使用上下文实例池很显然性能优于上下文,除此之外,二者在使用本质上并不存在太大性能差异。因为基于我们上述分析,若直接使用上下文,每次构建上下文实例,并不需要花费什么时间,同时,上下文实例池重用上下文后,也仅仅只是激活变更追踪属性,也不需要耗费什么时间。

这里我们也可以看到,上下文实例池和线程池区别很大,线程池重用线程,但创建线程开销可想而知,同时对于线程重用的机制也完全不一样,据我所知,线程池具有多个队列,对于线程池中的N个线程,有N+1个队列,每个线程都有一个本地队列和全局队列,至于选择哪个线程任务进入哪个队列看对应规则。

总结

分析至此,我们再对注入上下文和上下文实例池做一个完整的对比分析。上下文周期默认为Scope且可自定义,而上下文实例池所管理的上下文周期为Scope,无法再更改,上下文实例池默认大小为128,我们也可以重写其对应方法,若不给定maxSize(可空),则默认池大小为32。若上下文实例池队列存在可租赁上下文,则取出,然后仅仅只是激活变更追踪响应属性,否则直接创建上下文实例。若归还上下文超出上下文实例池队列大小(自定义池大小),则直接释放余下上下文,当然也就不再受上下文实例池所管理。

EntityFramework Core上下文实例池原理分析的更多相关文章

  1. Cookies 初识 Dotnetspider EF 6.x、EF Core实现dynamic动态查询和EF Core注入多个上下文实例池你知道有什么问题? EntityFramework Core 运行dotnet ef命令迁移背后本质是什么?(EF Core迁移原理)

    Cookies   1.创建HttpCookies Cookie=new HttpCookies("CookieName");2.添加内容Cookie.Values.Add(&qu ...

  2. EF 6.x、EF Core实现dynamic动态查询和EF Core实现多个上下文实例池你了解多少?

    前言 很长一段时间没有写博客了,今天补上一篇吧,偶尔发现不太愿意写博客了,太耗费时间,不过还是在坚持当中,毕竟或许写出来的东西能帮到一些童鞋吧,接下来我们直奔主题.无论是在在EF 6.x还是EF Co ...

  3. java并发包&线程池原理分析&锁的深度化

          java并发包&线程池原理分析&锁的深度化 并发包 同步容器类 Vector与ArrayList区别 1.ArrayList是最常用的List实现类,内部是通过数组实现的, ...

  4. Java 线程池原理分析

    1.简介 线程池可以简单看做是一组线程的集合,通过使用线程池,我们可以方便的复用线程,避免了频繁创建和销毁线程所带来的开销.在应用上,线程池可应用在后端相关服务中.比如 Web 服务器,数据库服务器等 ...

  5. 【java】-- 线程池原理分析

    1.为什么要学习使用多线程? 多线程的异步执行方式,虽然能够最大限度发挥多核计算机的计算能力,但是如果不加控制,反而会对系统造成负担. 线程本身也要占用内存空间,大量的线程会占用内存资源并且可能会导致 ...

  6. 【学习】005 线程池原理分析&锁的深度化

    线程池 什么是线程池 Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序 都可以使用线程池.在开发过程中,合理地使用线程池能够带来3个好处. 第一:降低资源消耗.通过重复 ...

  7. DBCP数据库连接池原理分析

    在比较大的项目中,需要不断的从数据库中获取数据,Java中则使用JDBC连接数据库,但是获取数据库的连接可是相当耗时的操作,每次连接数据库都获得 .销毁数据库连接,将是很大的一个开销.为了解决这种开销 ...

  8. DBCP连接池原理分析及配置用法

    DBCP连接池介绍 ----------------------------- 目前 DBCP 有两个版本分别是 1.3 和 1.4. DBCP 1.3 版本需要运行于 JDK 1.4-1.5 ,支持 ...

  9. 【转】DBCP连接池原理分析

    ---------------------------- 目前 DBCP 有两个版本分别是 1.3 和 1.4. DBCP 1.3 版本需要运行于 JDK 1.4-1.5 ,支持 JDBC 3. DB ...

随机推荐

  1. 手把手教你AspNetCore WebApi:入门

    需求 前几天,马老板给小明和小红一个"待办事项"网站,小明负责后端,小红负责前端,并要求网站可以同时在 Windows.和 Linux 上运行. 小明整理了一下"待办事项 ...

  2. 025 01 Android 零基础入门 01 Java基础语法 03 Java运算符 05 if条件结构

    025 01 Android 零基础入门 01 Java基础语法 03 Java运算符 05 if条件结构 本文知识点:Java中的if条件结构语句 关系运算符回顾 生活中根据条件进行判断采取不同操作 ...

  3. 《C++primerplus》第10章练习题

    1.定义一个类表示银行账户.数据成员包括姓名,账号和存款.成员函数可以执行初始化数据.显示数据和取款存款的功能. //Bank.cpp #include<iostream> #includ ...

  4. 介绍使用Cordova和Web Starter Kit开发Android

    介绍 如今,每个人都想制作移动应用程序,为什么不呢?世界上有更多的移动设备比任何其他用户设备.Android尤其流行,但是为什么不从一个众所周知的跨平台应用的基础开始呢?Android的开发显然比其他 ...

  5. RHSA-2018:0151-重要: 内核 安全和BUG修复更新(需要重启、存在EXP、本地提权)

    [root@localhost ~]# cat /etc/redhat-release CentOS Linux release 7.2.1511 (Core) 修复命令: 使用root账号登陆She ...

  6. 用 shell 脚本做 restful api 接口监控

    问题的提出 基于历史原因,公司有一个"三无"采集服务--无人员.无运维.无监控--有能力做的部门不想接.接了的部门没能力.于是就一直这样裸奔,直到前几天一个依赖于这个采集服务的大数 ...

  7. 利用HDFS实现ElasticSearch7.2容灾方案

    利用HDFS实现ElasticSearch7.2容灾方案 目录 利用HDFS实现ElasticSearch7.2容灾方案 前言 快照版本兼容 备份集群 HDFS文件系统 软件下载 JDK环境 配置系统 ...

  8. 为什么说switch比if快

    C++的switch语法 在C++中,switch只接受整型常量作为分支的值: switch (expr) { case integral-constant : \\... break; case i ...

  9. jmeter_04_常用取样器

    目录 常用取样器详解 http取样器 1.1 基本配置 1.2 高级配置 jdbc取样器 2.1 JDBC Connection Configuration 2.1.1 **Variable Name ...

  10. scrapy 采集数据存入excel

    # -*- coding: utf-8 -*- # Define your item pipelines here # # Don't forget to add your pipeline to t ...