前言

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

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

上下文实例池和线程池原理从概念来上讲一样,都是可重用,但在原理实现上却有本质区别。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. 64位系统 system32 和 syswow64

    \Windows\SysWOW64  文件夹下存放32位的库和应用程序 (WOW64 == Windows on Windows 64 bit ) \Windows\System32  文件夹下存放6 ...

  2. C# Redis分布式锁(RedLock) - 多节点

    Redis单节点的分布式锁只需要注意三点就可以了: 1.加锁并设置锁的过期时间必须是原子操作; 2.锁的value值必须要有唯一性; 3.释放锁的时候要验证其value值,不是自己加的锁不能释放. 但 ...

  3. 051 01 Android 零基础入门 01 Java基础语法 05 Java流程控制之循环结构 13 Eclipse下程序调试——debug入门1

    051 01 Android 零基础入门 01 Java基础语法 05 Java流程控制之循环结构 13 Eclipse下程序调试--debug入门1 本文知识点: 程序调试--debug入门1 程序 ...

  4. 「DevOps 转型与实践」沙龙回顾第一讲

    9 月 19 日,CODING 和中国 DevOps 社区联合举办的深圳第九届 Meetup 在腾讯大厦 2 楼多功能圆满结束.本次沙龙以 「DevOps 转型与实践」 为主题,4 位来自互联网.金融 ...

  5. IDEA设置External Tools之Javap反编译字节码

    通过Jdk的命令javap可以反编译查看字节码,但是在使用idea的时候一直用命令行去操作不太好操作,而且因为idea会把class码 放在target里面,经常会忘记切换目录.这个时候idea的Ex ...

  6. Go | Go 使用 consul 做服务发现

    Go 使用 consul 做服务发现 目录 Go 使用 consul 做服务发现 前言 一.目标 二.使用步骤 1. 安装 consul 2. 服务注册 定义接口 具体实现 测试用例 3. 服务发现 ...

  7. python实现自动生成小学四则运算题目(软工第二次项目作业)

    前言 软件工程 传送带 作业要求 传送带 作业目标 结对编程:代码实现.性能分析.异常处理说明.记录PSP表格 代码见: github 个人信息:朱育清 3118005437 信安二班 我的partn ...

  8. js改变,设置table单双行颜色,jquery改变,设置table单双行颜色

    1.js实现单双行以不同颜色显示 $(document).ready(function () { var color = "#ffeab3"; $("#GvList tr ...

  9. 干货分享:在Windows下使用Visual Studio搭建C语言开发环境

    前言:本文将教大家如何使用 VIsual Studio Code 搭建 C 语言开发环境,包括使用 VS Code 如何编译和调试 C 语言程序,需要 用到的工具有 Visual Studio Cod ...

  10. Cypress系列(68)- request() 命令详解

    如果想从头学起Cypress,可以看下面的系列文章哦 https://www.cnblogs.com/poloyy/category/1768839.html 作用 发起一个 HTTP 请求 语法格式 ...