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>)))
      {
        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。若上下文實例池隊列存在可租賃上下文,則取出,然後僅僅只是激活變更追蹤響應屬性,否則直接創建上下文實例。若歸還上下文超出上下文實例池隊列大小(自定義池大小),則直接釋放餘下上下文,當然也就不再受上下文實例池所管理。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章