有没有办法将已经打开的 MySQL 连接传递给 EF Core DbContext ?用于多线程目的

Posted

技术标签:

【中文标题】有没有办法将已经打开的 MySQL 连接传递给 EF Core DbContext ?用于多线程目的【英文标题】:Is there a way to pass an already opened MySQL Connection to EF Core DbContext ? For multithreading purpose 【发布时间】:2021-05-31 05:41:36 【问题描述】:

我已经苦苦挣扎了 2 天,为我的简单场景找到合适的解决方案。

我想要完成的场景 在 .Net Core Parallel 循环中执行多个数据库访问,这将在同一个数据库中插入多个项目。

var actions = new List<Action>();

actions.Add(() =>  new DbContext.Set<TEntity>().Add(entity); );
actions.Add(() =>  new DbContext.Set<TEntity>().Add(entity); );
actions.Add(() =>  new DbContext.Set<TEntity>().Add(entity); );
actions.Add(() =>  new DbContext.Set<TEntity>().Add(entity); );

Parallel.ForEach(actions, new ParallelOptions  MaxDegreeOfParallelism = 2 ,
action =>

     action();
);

已知限制

    EF Core 的 DBContext 不是线程安全的(我们需要在每个 trhead 中重新创建它们) mysql 服务器不接受使用同一事务建立的 2 个连接 似乎我们无法将已打开的连接传递给 DBContext EF Core 每次与数据库交互时都会在内部打开和关闭与数据库的连接

例外

多个同时连接或不同的连接 当前不在同一事务中的连接字符串 支持。

在 MySql.Data.MySqlClient.MySqlConnection.Open() 在 Microsoft.EntityFrameworkCore.Storage.RelationalConnection.OpenDbConnection(布尔 错误预期)在 Microsoft.EntityFrameworkCore.Storage.RelationalConnection.Open(布尔 错误预期)在 Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable1.Enumerator.BufferlessMoveNext(DbContext _, Boolean buffer) at Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable1.Enumerator.MoveNext() 在 Microsoft.EntityFrameworkCore.Query.Internal.LinqOperatorProvider._TrackEntities[TOut,TIn](IEnumerable1 results, QueryContext queryContext, IList1 entityTrackingInfos, IList1 entityAccessors)+MoveNext() at Microsoft.EntityFrameworkCore.Query.Internal.LinqOperatorProvider.ExceptionInterceptor1.EnumeratorExceptionInterceptor.MoveNext() 在 System.Collections.Generic.List1.AddEnumerable(IEnumerable1 可枚举)在 System.Linq.Enumerable.ToList[TSource](IEnumerable`1 来源)在 CloseTheMonth.Backend.Data.Repositories.AccountUserRightRepository.ListAccounts(Guid 用户 ID)在 C:\Work\GitHub\CloseTheMonth\Backend\CloseTheMonth.Data\Repositories\AccountUserRightRepository.cs:line 44 在 CloseTheMonth.Backend.Services.AccountUserRightService.ListAccounts(Guid 用户 ID)在 C:\Work\GitHub\CloseTheMonth\Backend\CloseTheMonth.Services\AccountUserRightService.cs:line 53 在 CloseTheMonth.Backend.Controllers.AppController.Init(字符串 授权,AppInitRequest 请求)在 C:\Work\GitHub\CloseTheMonth\Backend\CloseTheMonth.Backend\Controllers\AppController.cs:line 101

反思... 如果我可以像这样在全局范围内打开一个连接,并将其传递给我的 DBContexts,那就可以了。但是我检查了 EF Core 和 Pomelo 源代码,并没有找到实现这样的方法。

也许除了 Pomelo 之外的其他一些 EF Core MySQL 驱动程序可以做到这一点?

var actions = new List<Action>();

using (var conn = new MySqlConnection())

   actions.Add(() =>  new DbContext(conn).Set<TEntity>().Add(entity); );
   actions.Add(() =>  new DbContext(conn).Set<TEntity>().Add(entity); );
   actions.Add(() =>  new DbContext(conn).Set<TEntity>().Add(entity); );
   actions.Add(() =>  new DbContext(conn).Set<TEntity>().Add(entity); );

   Parallel.ForEach(actions, new ParallelOptions  MaxDegreeOfParallelism = 2 ,
   action =>
   
        action();
   );

我将 MySQL 服务器 (8.0.22) 与 Pomelo.EntityFrameworkCore.MySql (2.1.4) 一起使用

【问题讨论】:

你不能将这些元素添加到一个 ConcurrentBag 并在并行循环之后一次插入它们吗? 一开始就不需要在数据访问中使用多线程。您不能同时在同一个连接上执行多个操作,因此使用多个 DbContext 毫无意义。您需要多个连接。无论如何,哪个不会使缓慢或错误的查询运行得更快。 你到底想做什么?你写的没什么意义。 Add 不会对数据库进行任何更改。在调用SaveChanges 之前,所有更改都会被缓存,这会将所有更改保存在单个事务中。插入并不慢,所以如果您只有 4 个项目有问题,那么数据库有问题 - 可能是缺少索引?还是每次都必须扫描整个表的触发器/检查约束? 这能回答你的问题吗? C# Data Connections Best Practice? 你到底想做什么?你写的没什么意义。 Add 不会对数据库进行任何更改。在调用SaveChanges 之前,所有更改都会被缓存,这会将所有更改保存在单个事务中。插入并不慢,所以如果您只有 4 个项目有问题,那么数据库有问题 - 可能是缺少索引?还是每次都必须扫描整个表的触发器/检查约束? 【参考方案1】:

如果代码不是线程安全的,则必须在每个线程中使用单独的 MySQL 连接。

MySQL 协议是有状态的,因此如果查询-响应周期的一部分与针对不同查询的不同查询-响应周期交错,则响应会变得混乱。你不会喜欢这个结果。

设计使用数据库的多线程代码的唯一明智方法是让每个线程打开自己的连接。

【讨论】:

【参考方案2】:

现在一切都清楚了,让我继续说简单的:

    无法使用 EF Core 和 MySQL 进行多线程写入操作 可以使用 EF Core 和 MySQL 进行多线程读取操作

首先,写操作

DBContext 不是线程安全的,每个线程需要一个上下文 每次 Context.SaveChanges 都会打开和关闭一个连接 MySQL 拒绝在一个事务中打开多个连接 您不能在同一个数据库连接上执行多线程操作

由于您不能多线程插入/更新/删除,您当然可以通过避免每次触摸实体时调用 SaveChanges 并等到提交事务之前对其进行一些优化。

首先,只要您不保存更改,它将允许 EF 留在内存中。

其次,如果 EF Core 必须进行任何优化以喜欢、批量插入或任何可能完成的事情,它将能够做到,因为您将所有数据库作业保留到最后(所以 EF 知道要完成的工作量)。

二、读操作

DBContext 不是线程安全的,每个线程需要一个上下文 您需要在每个线程下创建一个作用域 依赖注入将为每个线程范围创建一个新的 UnitOfWork(因为您在 Startup 类中将其定义为 Scoped) 您同时执行所有线程,EF Core 将处理多个连接

对于读取操作,由于我希望代码看起来干净,这就是我所做的:

public class Multithreader : IDisposable

    private List<Action> _actions = new List<Action>();

    public Multithreader(int maxThreads)
    
        this._maxThreads = maxThreads;
    

    public void Enqueue(Action action)
    
        this._actions.Add(action);
    

    public void Dispose()
    
        Parallel.ForEach(this._actions, new ParallelOptions  MaxDegreeOfParallelism = 8 ,
        action =>
        
            action();
        );
    

我还在 BaseController 中创建了一个辅助函数来获取一个 Scoped Services 类(其中包含对我的服务的引用):

public class BaseController : ControllerBase

    private readonly IServiceProvider _serviceProvider;

    public BaseController(IServiceProvider serviceProvider)
    
        this._serviceProvider = serviceProvider;
    

    protected IServices GetScopedServices()
    
        var scope = _serviceProvider.CreateScope();

        return scope.ServiceProvider.GetService<IServices>();
    

然后我只是把所有东西都排入队列以获得我想要的东西:

using (var threader = new Multithreader())

    threader.Enqueue(() =>  using (var services = this.GetScopedServices())  entity.Value1 = services.Accounts.GetValue1();  );
    threader.Enqueue(() =>  using (var services = this.GetScopedServices())  entity.Value2 = services.Accounts.GetValue2();  );
    threader.Enqueue(() =>  using (var services = this.GetScopedServices())  entity.Value3 = services.Accounts.GetValue3();  );
    threader.Enqueue(() =>  using (var services = this.GetScopedServices())  entity.Value4 = services.Accounts.GetValue4();  );
    threader.Enqueue(() =>  using (var services = this.GetScopedServices())  entity.Value5 = services.Accounts.GetValue5();  );
    threader.Enqueue(() =>  using (var services = this.GetScopedServices())  entity.Value6 = services.Accounts.GetValue6();  );
    threader.Enqueue(() =>  using (var services = this.GetScopedServices())  entity.Value7 = services.Accounts.GetValue7();  );
    threader.Enqueue(() =>  using (var services = this.GetScopedServices())  entity.Value8 = services.Accounts.GetValue8();  );
    threader.Enqueue(() =>  using (var services = this.GetScopedServices())  entity.Value9 = services.Accounts.GetValue9();  );

【讨论】:

这里有几点说明:官方完全不支持对DbContext 的多线程访问。话虽如此,到目前为止,多个读取操作通常仍然有效。调用DbContext.SaveChanges 只会打开一个连接,如果它还没有打开的话。它永远不会关闭连接。仅在释放上下文时才关闭连接(并且仅当 EF Core 是打开它的那个时)。每个线程有一个专用的DbContext 绝对是实现多个并发写入操作的最简单方法(如果你真的需要它们是并发的,大多数人不这样做)。

以上是关于有没有办法将已经打开的 MySQL 连接传递给 EF Core DbContext ?用于多线程目的的主要内容,如果未能解决你的问题,请参考以下文章

有没有办法将 POM 参数/属性传递给 TestNG XML 文件?

将 MySQL 连接传递给函数

有没有办法将变量传递给 Django 中的“扩展”模板?

有没有办法将变量传递给 Jinja2 父母?

有没有办法将时间权重传递给损失函数?

有没有办法将数组传递给外部 REXX 程序?