EntityFramework Core上下文实例池原理
【導(dǎo)讀】無(wú)論是在我個(gè)人博客還是著作中,對(duì)于上下文實(shí)例池都只是通過(guò)大量文字描述來(lái)講解其基本原理,而且也是淺嘗輒止,導(dǎo)致我們對(duì)其認(rèn)識(shí)仍是一知半解,本文我們擺源碼,從源頭開始分析
希望通過(guò)本文從源碼的分析,我們大家都能了解到上注入下文和上下文實(shí)例池的區(qū)別在哪里,什么時(shí)候用上下文,什么時(shí)候用上下文實(shí)例池
友情提醒:此文略長(zhǎng),若心情煩躁、郁悶,無(wú)法靜心,可以直接滑至文末總結(jié)或另安排時(shí)間再詳細(xì)閱讀本文
上下文實(shí)例池原理準(zhǔn)備工作
上下文實(shí)例池和線程池原理從概念來(lái)上講一樣,都是可重用,但在原理實(shí)現(xiàn)上卻有本質(zhì)區(qū)別。EF Core定義上下文實(shí)例池接口即IDbContextPool,將其接口實(shí)現(xiàn)抽象為:租賃(Rent)和歸還(Return)。如下:
public?interface?IDbContextPool {DbContext?Rent();bool?Return([NotNull]?DbContext?context); }那么租賃和歸還的機(jī)制是什么呢?接下來(lái)我們從注入上下文實(shí)例池開始講解。
當(dāng)我們?cè)赟tartup中注入上下文和上下文實(shí)例池時(shí),其他參數(shù)配置我們暫且忽略,從使用上二者最大區(qū)別在于,上下文可自定義設(shè)置生命周期,默認(rèn)為Scope,而上下文實(shí)例池可自定義最大池大小,默認(rèn)為128。
那么問(wèn)題來(lái)了,上下文實(shí)例池所管理的上下文的生命周期到底是什么呢?我們一探源碼究竟,參數(shù)細(xì)節(jié)判斷部分這里忽略分析
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()));} }首先判斷上下文必須有構(gòu)造函數(shù),因存在隱式默認(rèn)無(wú)參構(gòu)造函數(shù),所以繼續(xù)增強(qiáng)判斷,構(gòu)造函數(shù)參數(shù)不能為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,因每個(gè)上下文無(wú)論實(shí)例化多少次,其DbContextOptions不會(huì)發(fā)生改變
serviceCollection.TryAddSingleton(sp?=>?new?DbContextPool<TContextImplementation>(sp.GetService<DbContextOptions<TContextImplementation>>()));然后,以單例形式注入上下文實(shí)例池接口實(shí)現(xiàn),因?yàn)樵搶?shí)例中存在隊(duì)列機(jī)制來(lái)維護(hù)上下文,所有此類必然為單例,同時(shí),該實(shí)例需要用到DbContextOptions,所以提前注入DbContextOptions
serviceCollection.AddScoped<DbContextPool<TContextImplementation>.Lease>();緊接著,以生命周期為Scope注入Lease類,此類作為上下文實(shí)例池嵌套密封類存在,從單詞理解就是對(duì)上下文進(jìn)行釋放(歸還)處理(接下來(lái)會(huì)講到)
serviceCollection.AddScoped(sp?=>?(TContextService)sp.GetService<DbContextPool<TContextImplementation>.Lease>().Context);最后,這里就是上下文實(shí)例池所管理的上下文,其生命周期為Scope,不可更改
上下文實(shí)例池原理構(gòu)造實(shí)現(xiàn)
首先給出上下文實(shí)例池中重要屬性,以免越往下看一臉懵
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;上述是對(duì)于注入上下文實(shí)例池所做的準(zhǔn)備工作,接下來(lái)我們則來(lái)到上下文實(shí)例池具體實(shí)現(xiàn)
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()));} }在其構(gòu)造中,獲取自定義實(shí)例池最大大小,若未設(shè)置則以DefaultPoolSize為準(zhǔn),DefaultPoolSize定義為常量32
然后,防止實(shí)例化上下文后DbContextOptions配置發(fā)生更改,此時(shí)調(diào)用Freeze方法進(jìn)行凍結(jié)
接下來(lái)則是實(shí)例化上下文,此時(shí)將其包裹在委托中,還未真正實(shí)例化,繼續(xù)看上述CreateActivator方法實(shí)現(xiàn)
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>))){returnExpression.Lambda<Func<TContext>>(Expression.New(constructors[0],?Expression.Constant(options))).Compile();}}return?null; }簡(jiǎn)言之,上下文構(gòu)造函數(shù)和參數(shù)有且只能有一個(gè),而且參數(shù)必須類型必須是DbContextOptions,最后通過(guò)lambda表達(dá)式構(gòu)造上下文委托
通過(guò)上述分析,正常情況下,我們知道設(shè)計(jì)如此,上下文只能是顯式有參構(gòu)造,而且參數(shù)必須只能有一個(gè)且必須是DbContextOptions。
但有些情況下,我們?cè)谏舷挛臉?gòu)造中確實(shí)需要使用注入實(shí)例,豈不玩不了,若存在這種需求,這里請(qǐng)參考之前文章(EntityFramework Core 3.x上下文構(gòu)造函數(shù)可以注入實(shí)例呢?)
上下文實(shí)例池原理本質(zhì)實(shí)現(xiàn)
上下文實(shí)例池構(gòu)造得到最大實(shí)例池大小以及構(gòu)造上下文委托(并未真正使用),接下來(lái)則是對(duì)上下文進(jìn)行租賃(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; }從上下文實(shí)例池中的隊(duì)列去獲取上下文,很顯然,首次沒(méi)有,于是就激活上下文委托,實(shí)例化上下文
若存在則將_count減1,然后將上下文的狀態(tài)進(jìn)行激活或復(fù)活處理
_count屬性用來(lái)與獲取到的實(shí)例池大小maxSize進(jìn)行比較(至于如何比較,接下來(lái)歸還用講到),然后為防并發(fā)線程中斷等機(jī)制,不能用簡(jiǎn)單的_count--,必須保持其原子性,所以用Interlocked,不清楚這個(gè)用法,補(bǔ)補(bǔ)基礎(chǔ)
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; }當(dāng)上下文釋放時(shí)(釋放時(shí)做什么處理,下面會(huì)講),首先將上下文狀態(tài)重置,無(wú)非就是將上下文所跟蹤的模型(變更追蹤機(jī)制)進(jìn)行關(guān)閉處理等等,這里就不做深入探討,接下來(lái)則是將上下文歸還上下文到隊(duì)列中。
我們結(jié)合租賃和歸還整體分析:設(shè)置池大小為32,若此時(shí)有33個(gè)請(qǐng)求,且處理時(shí)間較長(zhǎng),此時(shí)將直接租賃33個(gè)上下文,緊接著33個(gè)上下文陸續(xù)被釋放,此時(shí)開始將0-31歸還入隊(duì)列,當(dāng)索引為32時(shí),此時(shí)_count為33,無(wú)法入隊(duì),怎么搞?此時(shí)將來(lái)到注入的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;} }若請(qǐng)求超出自定義池大小,且請(qǐng)求處理周期很長(zhǎng),那么在釋放時(shí),余下上下文則不能再歸還如隊(duì)列,將直接釋放,同時(shí)上下文實(shí)例池將結(jié)束掉自身不再具備對(duì)該上下文的維護(hù)處理能力
我們?cè)俅位氐阶赓U方法,當(dāng)隊(duì)列中存在可用的上下文時(shí),可以知道每次都重新實(shí)例化一個(gè)上下文和上下文實(shí)例池管理上下文的本質(zhì)區(qū)別在于對(duì)Resurrect方法的處理。
?((IDbContextPoolable)context).Resurrect(_configurationSnapshot);我們?cè)賮?lái)看看該方法具體處理情況怎樣,是否存在什么魔法從而有所影響性能的地方,我們?cè)谥付▓?chǎng)景必須使用實(shí)例池呢?
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;} }哇,我們驚呆了,完全沒(méi)啥,都不用我們?cè)俳忉?#xff0c;只是簡(jiǎn)單設(shè)置變更追蹤各個(gè)狀態(tài)屬性而已
毫無(wú)疑問(wèn),上下文實(shí)例確實(shí)可以重用上下文實(shí)例,若存在復(fù)雜的業(yè)務(wù)邏輯和吞吐量比較大的情況,使用上下文實(shí)例池很顯然性能優(yōu)于上下文,除此之外,二者在使用本質(zhì)上并不存在太大性能差異
因?yàn)榛谖覀兩鲜龇治?#xff0c;若直接使用上下文,每次構(gòu)建上下文實(shí)例,并不需要花費(fèi)什么時(shí)間,同時(shí),上下文實(shí)例池重用上下文后,也僅僅只是激活變更追蹤屬性,也不需要耗費(fèi)什么時(shí)間。
這里我們也可以看到,上下文實(shí)例池和線程池區(qū)別很大,線程池重用線程,但創(chuàng)建線程開銷可想而知,同時(shí)對(duì)于線程重用的機(jī)制也完全不一樣,據(jù)我所知,線程池具有多個(gè)隊(duì)列,對(duì)于線程池中的N個(gè)線程,有N+1個(gè)隊(duì)列,每個(gè)線程都有一個(gè)本地隊(duì)列和全局隊(duì)列,至于選擇哪個(gè)線程任務(wù)進(jìn)入哪個(gè)隊(duì)列看對(duì)應(yīng)規(guī)則
分析至此,我們?cè)賹?duì)注入上下文和上下文實(shí)例池做一個(gè)完整的對(duì)比分析
?????上下文周期默認(rèn)為Scope且可自定義,而上下文實(shí)例池所管理的上下文周期為Scope,無(wú)法再更改
?????上下文實(shí)例池默認(rèn)大小為128,我們也可以重寫其對(duì)應(yīng)方法,若不給定maxSize(可空),則默認(rèn)池大小為32
?????若上下文實(shí)例池隊(duì)列存在可租賃上下文,則取出,然后僅僅只是激活變更追蹤響應(yīng)屬性,否則直接創(chuàng)建上下文實(shí)例
?????若歸還上下文超出上下文實(shí)例池隊(duì)列大小(自定義池大小),則直接釋放余下上下文,當(dāng)然也就不再受上下文實(shí)例池所管理
總結(jié)
以上是生活随笔為你收集整理的EntityFramework Core上下文实例池原理的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: C# 8: 可变结构体中的只读实例成员
- 下一篇: 部署Dotnet Core应用到Kube