由于DbContext不支持多线程的, 不能让DbContext被多个进程同时使用, 所有要实现并发的插入修改删除操作的关键在于创建多个DbContext, 确保每一个DbContext只执行一个任务

例子

  1. .NET 框架自带依赖注入容器管理对象的生命周期, 我们可以直接使用IServiceScopeFactory实例来获取多个DbContext实例

    [Route("api/[controller]")]
    public class OrdersController : ControllerBase
    {
    private readonly IServiceScopeFactory _scopeFactory;
    public OrdersController(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }
    }
  2. 并发操作, 确保一个DbContext对应着一个任务

    [HttpPost("parallel-safe")]
    public async Task<IActionResult> ParallelSafe([FromBody] List<Order> ordersToInsert)
    {
    // 分批并发插入,例如每批 10 个任务,每个任务独立 Scope
    var tasks = new List<Task>();
    
    // 假设 ordersToInsert 包含 100 个订单,每个任务处理一部分
    var batchSize = 10;
    for (int i = 0; i < ordersToInsert.Count; i += batchSize)
    {
        var batch = ordersToInsert.Skip(i).Take(batchSize).ToList();
        tasks.Add(Task.Run(async () =>
        {
            // ✅ 每个并发任务独立创建 Scope 和 Repository
            using (var scope = _scopeFactory.CreateScope())
            {
                var repo = scope.ServiceProvider.GetRequiredService<Repository<Order>>();
                foreach (var order in batch)
                {
                    await repo.AddAsync(order);
                }
                await repo.SaveAsync();  // 每个任务在自己的 DbContext 上 SaveChanges
            }
        }));
    }
    
    await Task.WhenAll(tasks);
    return Ok($"Inserted {ordersToInsert.Count} orders in parallel.");
    }

进阶操作, 改为使用DbContextFactory来获取DbContext实例

设计优化, 为了实现并发操作就获取_scopeFactory来获取DbContext实例耦合度有点高, 推荐使用功能更加内聚, 职责更加专注于DbContext实例创建的DbContextFactory更加优雅

  1. 注册DbContextFactory
    //使用AddDbContextFactory注册工厂,确保每次获取的DbContext实例都是独立的
    builder.Services.AddDbContextFactory<Test2Context>(options =>
    {
    options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString));
    });

注意事项:

  1. DbContextFactory默认情况是单例服务, 也不推荐将其注册成其他服务

  2. 不推荐DbContextDbContextFactory, 同时注册, 会出现工厂在构建的时候获取一个生命周期为Scoped的DbContextOptions时 就会触发了 .NET Core 依赖注入(DI)的“作用域服务无法被单例服务消费”的限制。

  3. 如果一定要同时注册, 可以将DbContextFactory注册成域生命周期, 但不推荐这种行为

    // 使用AddDbContextFactory注册工厂,确保每次获取的DbContext实例都是独立的
    builder.Services.AddDbContextFactory<Test2Context>(options =>
    {
    options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString));
    }, ServiceLifetime.Scoped);
  4. 获取DbContextFactory实例, 假设我们在Repository<T>类里操作

    public class Repository<TEntity> where TEntity : IEntity, new()
    {
    private readonly IDbContextFactory _dbContextFactory;
    public Repository(IDbContextFactory dbContextFactory)
    {
        _dbContextFactory = dbContextFactory;
    }
    }
  5. 并发操作

    public async Task<bool> ParallelSafe(List<Order> ordersToInsert)
    {
    // 分批并发插入,例如每批 10 个任务,每个任务独立 Scope
    var tasks = new List<Task>();
    
    // 假设 ordersToInsert 包含 100 个订单,每个任务处理一部分
    var batchSize = 10;
    for (int i = 0; i < ordersToInsert.Count; i += batchSize)
    {
        var batch = ordersToInsert.Skip(i).Take(batchSize).ToList();
        tasks.Add(Task.Run(async () =>
        {
            // ✅ 每个并发任务独立创建context
            using (var context = _dbContextFactory.CreateDbContext())
            {
                await context.AddRangeAsync(batch);
                await repo.SaveAsync();  // 每个任务在自己的 DbContext 上 SaveChanges
            }
        }));
    }
    
    await Task.WhenAll(tasks);
    return true;
    }

进阶操作, 改为使用DbContextPool来实现DbContext池化操作

DbContext本身的创建和销毁行为本身并不直接影响系统与数据库的连接数, 对性能的损耗也很小, 但是在高性能场景, 这种损耗不可估量. 推荐使用DbContextPool来实现DbContext池化操作, 相较于前面的DbContextFactory来说, 这是一种成本更低, 性能更好的措施.

  1. 注册DbContextPool
    builder.Services.AddDbContextPool<Test2Context>(options =>
    {
    options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString));
    });

注意事项:

  1. DbContextFactory一样, DbContextPool也是单例服务

  2. DbContextFactory一样, 注册了DbContextPool就不需要直接注册DbContext了, 其内部方法会注册的

  3. DbContextFactoryDbContextPool相互不冲突, 可以同时注册他们并同时使用他们

  4. DbContextFactory不同的是, 注册了DbContextPool就完成了对DbContext池化操作, 只需要像是正常对DbContext编写并行操作即可

  5. DbContextPool的poolsize默认大小为1024,要确保你使用数据库能接受的最大连接数的大小, 推荐poolsize的大小不超过你的数据库最大连接书的80%, 以及通过基准测试来确定最适合的poolsize的值

    builder.Services.AddDbContextPool<Test2Context>(options =>
    {
    options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString));
    }, 64); // 设定poolsize为64
  6. 获取IServiceScopeFactory实例来获取多个DbContext实例

    public class Repository<TEntity> where TEntity : IEntity, new()
    {
    private readonly IServiceScopeFactory _scopeFactory;
    public OrdersController(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }
    }
  7. 并发操作

    public async Task<bool> ParallelSafe(List<Order> ordersToInsert)
    {
    // 分批并发插入,例如每批 10 个任务,每个任务独立 Scope
    var tasks = new List<Task>();
    
    // 假设 ordersToInsert 包含 100 个订单,每个任务处理一部分
    var batchSize = 10;
    for (int i = 0; i < ordersToInsert.Count; i += batchSize)
    {
        var batch = ordersToInsert.Skip(i).Take(batchSize).ToList();
        tasks.Add(Task.Run(async () =>
        {
            // ✅ 每个并发任务独立创建 Scope 和 Repository
            using (var scope = _scopeFactory.CreateScope())
            {
                var context = scope.ServiceProvider.GetRequiredService<>();
                await context.AddRangeAsync(batch);
                await context.SaveAsync();  // 每个任务在自己的 DbContext 上 SaveChanges
            }
        }));
    }
    
    await Task.WhenAll(tasks);
    return true;
    }
  8. 或者使用DbContextFactory实例来获取多个DbContext实例

    public class Repository<TEntity> where TEntity : IEntity, new()
    {
    private readonly IDbContextFactory _dbContextFactory;
    public Repository(IDbContextFactory dbContextFactory)
    {
        _dbContextFactory = dbContextFactory;
    }
    }
  9. 并发操作

    public async Task<bool> ParallelSafe(List<Order> ordersToInsert)
    {
    // 分批并发插入,例如每批 10 个任务,每个任务独立 Scope
    var tasks = new List<Task>();
    
    // 假设 ordersToInsert 包含 100 个订单,每个任务处理一部分
    var batchSize = 10;
    for (int i = 0; i < ordersToInsert.Count; i += batchSize)
    {
        var batch = ordersToInsert.Skip(i).Take(batchSize).ToList();
        tasks.Add(Task.Run(async () =>
        {
            // ✅ 每个并发任务独立创建context
            using (var context = _dbContextFactory.CreateDbContext())
            {
                await context.AddRangeAsync(batch);
                await repo.SaveAsync();  // 每个任务在自己的 DbContext 上 SaveChanges
            }
        }));
    }
    
    await Task.WhenAll(tasks);
    return true;
    }