EntityFramework Core上下文实例池原理
【導讀】無論是在我個人博客還是著作中,對于上下文實例池都只是通過大量文字描述來講解其基本原理,而且也是淺嘗輒止,導致我們對其認識仍是一知半解,本文我們擺源碼,從源頭開始分析
希望通過本文從源碼的分析,我們大家都能了解到上注入下文和上下文實例池的區別在哪里,什么時候用上下文,什么時候用上下文實例池
友情提醒:此文略長,若心情煩躁、郁悶,無法靜心,可以直接滑至文末總結或另安排時間再詳細閱讀本文
上下文實例池原理準備工作
上下文實例池和線程池原理從概念來上講一樣,都是可重用,但在原理實現上卻有本質區別。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>))){returnExpression.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上下文实例池原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C# 8: 可变结构体中的只读实例成员
- 下一篇: 部署Dotnet Core应用到Kube