EntityFramework多线程安全和连接池问题探究

一、多线程操作数据库出现的问题

因为EntityFramework的DataContext不是线程安全的,所有多线程使用EntityFramework的DataContext遇到了下面错误

“A second operation was started on this context before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.”

即使使用原生的MySqlConnection对象,多线程访问同样会出现问题
测试代码

using DependencyInjection;
using EntityDbService.Models;
using EntityDbService.Table;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Share.SystemParameter;

namespace DataBaseTest
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var serviceProvider = new ServiceCollection()
                    .AddDbContext<DataContext>(optionsBuilder => optionsBuilder.UseMySql(Parameters.GetConnectionString(), new MySqlServerVersion(new Version(5, 7, 0))))
                    .BuildServiceProvider();
            SingleService.SetServiceProvider(serviceProvider);
            StartEntityTest();
        }

        private static List<Thread> _threads = new List<Thread>();
        private static bool _isStopThread = false;

        public static void StartEntityTest()
        {
            Thread addThread1 = new Thread(AddThread);
            _threads.Add(addThread1);
            addThread1.Start();

            Thread updateThread1 = new Thread(UpdateThread);
            _threads.Add(updateThread1);
            updateThread1.Start();

            Thread selectThread1 = new Thread(SelectThread);
            _threads.Add(selectThread1);
            selectThread1.Start();
        }

        public static void AddThread()
        {
            var dataContext = SingleService.Services.GetService<DataContext>();
            while (!_isStopThread)
            {
                try
                {
                    EquipmentTable equipmentTable = new EquipmentTable();
                    equipmentTable.Name = "test";
                    equipmentTable.ImeiStr = "123456789";
                    equipmentTable.Timestamp = DateTime.Now;
                    equipmentTable.CompanyId = 0;
                    dataContext.EquipmentTables.Add(equipmentTable);
                    var count = dataContext.SaveChanges();
                    Console.WriteLine($"AddThread:{Thread.GetCurrentProcessorId},equipmentTable.SaveChanges():{count}");
                    Thread.Sleep(100);
                }
                catch(Exception ex)
                {
                    Console.WriteLine($"AddThread:{Thread.GetCurrentProcessorId},Exception:{ex.ToString()}");
                    Thread.Sleep(1000);
                }
            }
        }

        public static void UpdateThread()
        {
            var dataContext = SingleService.Services.GetService<DataContext>();
            while (!_isStopThread)
            {
                try
                {
                    Random random = new Random();
                    var id = random.Next(1, 10000);
                    var equipmentTable = dataContext.EquipmentTables.Find(id);
                    if (equipmentTable != null)
                    {
                        equipmentTable.CompanyId = equipmentTable.CompanyId + 1;
                        var count = dataContext.SaveChanges();
                        Console.WriteLine($"UpdateThread:{Thread.GetCurrentProcessorId},equipmentTable.SaveChanges():{count}");
                    }
                    else
                    {
                        Console.WriteLine($"UpdateThread:{Thread.GetCurrentProcessorId},equipmentTable.SaveChanges():{0}");
                    }
                    Thread.Sleep(100);
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"UpdateThread:{Thread.GetCurrentProcessorId},Exception:{ex.ToString()}");
                    Thread.Sleep(1000);
                }
            }
        }

        public static void SelectThread()
        {
            var dataContext = SingleService.Services.GetService<DataContext>();
            while (!_isStopThread)
            {
                try
                {
                    Random random = new Random();
                    var id = random.Next(1, 10000);
                    var equipmentTable = dataContext.EquipmentTables.Skip(id).Take(id * 10);
                    if (equipmentTable != null && equipmentTable.Count() > 0)
                    {
                        Console.WriteLine($"SelectThread:{Thread.GetCurrentProcessorId},equipmentTable.Count():{equipmentTable.Count()}");
                    }
                    else
                    {
                        Console.WriteLine($"SelectThread:{Thread.GetCurrentProcessorId},equipmentTable.Count():{0}");
                    }
                    Thread.Sleep(100);
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"SelectThread:{Thread.GetCurrentProcessorId},Exception:{ex.ToString()}");
                    Thread.Sleep(1000);
                }
            }
        }
    }
}

二、第一个想法,为每个数据表创建一个DataContext,然后加锁

创建多个DataContext

.AddDbContext<DataContext>(optionsBuilder => optionsBuilder.UseMySql(ConnectionString, new MySqlServerVersion(new Version(5, 7, 0))))
.AddDbContext<EquipmentContext>(optionsBuilder => optionsBuilder.UseMySql(ConnectionString, new MySqlServerVersion(new Version(5, 7, 0))))

很显然是不可行的

PM> Add-Migration init
Build started...
Build succeeded.
More than one DbContext was found. Specify which one to use. Use the '-Context' parameter for PowerShell commands and the '--context' parameter for dotnet commands.

三、第二个想法,全局锁,很显然可以用,但是性能不行

测试代码

using EntityDbService.Table;
using Microsoft.EntityFrameworkCore;

namespace EntityDbService.Models
{
    public class DataContext : DbContext
    {

        public DataContext(DbContextOptions<DataContext> options)
            : base(options)
        {

            if (Database.GetPendingMigrations().Any())
            {
                Database.Migrate(); //执行迁移
            }
        }

        public DbSet<EquipmentTable> EquipmentTables { get; set; }
        public object Lock { get => _sign; set => _sign = value; }
        //锁
        private object _sign = new object();
    }
}

使用时要全部加锁

using DependencyInjection;
using EntityDbService.Models;
using EntityDbService.Table;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DataBaseTest
{
    public static class EntityTest
    {
        private static List<Thread> _threads = new List<Thread>();
        private static bool _isStopThread = false;
        private static int _time = 10;

        public static void StartEntityTest()
        {
            Thread addThread1 = new Thread(AddThread);
            _threads.Add(addThread1);
            addThread1.Start();

            Thread addThread2 = new Thread(AddThread);
            _threads.Add(addThread2);
            addThread2.Start();

            Thread updateThread1 = new Thread(UpdateThread);
            _threads.Add(updateThread1);
            updateThread1.Start();

            Thread updateThread2 = new Thread(UpdateThread);
            _threads.Add(updateThread2);
            updateThread2.Start();

            Thread selectThread1 = new Thread(SelectThread);
            _threads.Add(selectThread1);
            selectThread1.Start();

            Thread selectThread2 = new Thread(SelectThread);
            _threads.Add(selectThread2);
            selectThread2.Start();
        }

        public static void AddThread()
        {
            var dataContext = SingleService.Services.GetService<DataContext>();
            while (!_isStopThread)
            {
                try
                {
                    int count = 0;
                    lock (dataContext.Lock)
                    {
                        EquipmentTable equipmentTable = new EquipmentTable();
                        equipmentTable.Name = "test";
                        equipmentTable.ImeiStr = "123456789";
                        equipmentTable.Timestamp = DateTime.Now;
                        equipmentTable.CompanyId = 0;
                        dataContext.EquipmentTables.Add(equipmentTable);
                        count = dataContext.SaveChanges();
                    }
                    Console.WriteLine($"AddThread:{Thread.GetCurrentProcessorId()},equipmentTable.SaveChanges():{count}");
                    Thread.Sleep(_time);
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"AddThread:{Thread.GetCurrentProcessorId()},Exception:{ex.ToString()}");
                    Thread.Sleep(1000);
                }
            }
        }

        public static void UpdateThread()
        {
            var dataContext = SingleService.Services.GetService<DataContext>();
            while (!_isStopThread)
            {
                try
                {
                    Random random = new Random();
                    var id = random.Next(1, 10000);
                    var count = 0;
                    lock (dataContext.Lock)
                    {
                        EquipmentTable equipmentTable = dataContext.EquipmentTables.Find(id);
                        if (equipmentTable != null)
                        {
                            equipmentTable.CompanyId = equipmentTable.CompanyId + 1;
                            count = dataContext.SaveChanges();
                        }
                    }
                    Console.WriteLine($"UpdateThread:{Thread.GetCurrentProcessorId()},equipmentTable.SaveChanges():{count}");
                    Thread.Sleep(_time);
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"UpdateThread:{Thread.GetCurrentProcessorId()},Exception:{ex.ToString()}");
                    Thread.Sleep(1000);
                }
            }
        }

        public static void SelectThread()
        {
            var dataContext = SingleService.Services.GetService<DataContext>();
            while (!_isStopThread)
            {
                try
                {
                    Random random = new Random();
                    var id = random.Next(1, 10000);
                    lock (dataContext.Lock)
                    {
                        var equipmentTable = dataContext.EquipmentTables.Skip(id).Take(10);
                   
                        if (equipmentTable != null && equipmentTable.Count() > 0)
                        {
                            Console.WriteLine($"SelectThread:{Thread.GetCurrentProcessorId()},equipmentTable.Count():{equipmentTable.Count()}");
                        }
                        else
                        {
                            Console.WriteLine($"SelectThread:{Thread.GetCurrentProcessorId()},equipmentTable.Count():{0}");
                        }
                    }
                    Thread.Sleep(_time);
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"SelectThread:{Thread.GetCurrentProcessorId()},Exception:{ex.ToString()}");
                    Thread.Sleep(1000);
                }
            }
        }
    }
}

四、第三个想法,建立一个数据库连接池,微软已经实现,只需要配置即可

池中每个实例都是一个独立的数据库连接,每次要使用时都去池中请求空闲连接,因为连接多所以性能较单个数据库连接加锁要好很多,如果都没有空闲,可以通过机制进行等待或者增加连接池中连接。

sql设置

启用连接池

string connStringUsePool = "server=.;database=master;uid=sa;pwd=123;pooling=true;connection lifetime=0;min pool size = 1;max pool size=500";

mysql设置

启用连接池

string connStringUsePool = "server=127.0.0.1;uid=root;pwd=xxx;database=databasetest;pooling=true;connectionlifetime=5;minpoolsize=1;maxpoolsize=500;"

DataContext代码

using EntityDbService.Table;
using Microsoft.EntityFrameworkCore;
using Share.SystemParameter;

namespace EntityDbService.Models
{
    public class DataContext : DbContext
    {
        public void Migrate()
        {
            if (Database.GetPendingMigrations().Any())
            {
                Database.Migrate(); //执行迁移
            }
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseMySql(Parameters.GetConnectionString(), new MySqlServerVersion(new Version(5, 7, 0)));
        }

        public DbSet<EquipmentTable> EquipmentTables { get; set; }
    }
}

EquipmentTable类代码

public class EquipmentTable
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }
    public int GroupId { get; set; }
    public int CompanyId { get; set; }
    public string Name { get; set; } = String.Empty;
}

SchoolContextFactory类代码,用来生成脚本的

namespace EntityDbService.Models
{
    public class SchoolContextFactory : IDesignTimeDbContextFactory<DataContext>
    {
        // IConfiguration Configuration { get; } //使用Configuration 获取不到GetConnectionString("SchoolContext")。不能用
        public DataContext CreateDbContext(string[] args)
        {
            //var optionsBuilder = new DbContextOptionsBuilder<DataContext>();
            //string ConnectionString = Parameters.GetConnectionString();
            //optionsBuilder.UseMySql(ConnectionString, new MySqlServerVersion(new Version(5, 7, 0)));
            //return new DataContext(optionsBuilder.Options);
            return new DataContext();
        }
    }
}

使用,如果并发过高会抛出Too many connections太多的连接

try
{
    using (DataContext dataContext = new DataContext())
    {
        EquipmentTable equipmentTable = new EquipmentTable();
        equipmentTable.Name = "test";
        equipmentTable.ImeiStr = "123456789";
        equipmentTable.CompanyId = 0;
        dataContext.EquipmentTables.Add(equipmentTable);
        dataContext.SaveChanges();
    }
}
catch (Exception ex)
{
    Console.WriteLine($"AddThread:{Thread.GetCurrentProcessorId()},Exception:{ex.ToString()}");
    Thread.Sleep(1000);
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
好的,让我为您解答关于entityframework core多线程安全添加数据的问题。 首先,为了保证多线程添加数据的安全性,我们需要采用下列方法之一: 1. 使用隔离级别为Serializable的数据库事务,这种隔离级别会锁定所有涉及的资源,直到事务完成,可以避免多个线程之间同时访问同一资源的问题。 2. 使用线程同步机制,比如使用lock关键字保护代码块,避免多个线程同时访问同一资源的问题。 在 Code First 设计模式中,我们可以使用 DbSet<TEntity> 类来代表一个实体集,从而方便地对实体对象进行各种操作。 在多线程环境下,我们应该创建不同的 DbContext 实例,并且别忘了使用 using 语句来自动释放资源。下面是一个使用 Code First 设计模式添加实体的多线程示例: ``` csharp public async Task AddEntityAsync<TEntity>(TEntity entity) where TEntity : class { using (var db = new MyDbContext()) { try { await db.Database.BeginTransactionAsync(IsolationLevel.Serializable); await db.Set<TEntity>().AddAsync(entity); await db.SaveChangesAsync(); await db.Database.CommitTransactionAsync(); } catch(Exception e) { await db.Database.RollbackTransactionAsync(); throw e; } } } ``` 在上面的代码中,我们使用了 IsolationLevel.Serializable 隔离级别来保证事务的粒度,同时使用 try..catch..finally 语句来保证事务的完整性,即事务在出现异常时回滚。 希望以上答案可以帮助您解决问题,谢谢。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

花开花落的个人博客

你的鼓励是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值