背景

我正在尝试编写一个与SQLite DB一起使用的异​​步服务器。我正在将.NET Core与Entity Framework Core一起使用。

我正在将UnitOfWork与GenericRepository模式一起使用,但是如下面的示例代码所示,这与这些模式并没有真正的关系。

我正在使用Windows 10,但是我希望任何受支持的.NET Core平台都具有相同的行为。

我想实现的目标

我要实现的是合理的交易行为。我将整个问题简化为一个简单的方案,在该方案中,我调查数据库中是否存在某个对象,如果不存在,则添加该对象,否则将失败。整个操作都在一个事务中,并且预期的场景是:


  • 该对象在数据库中不存在。
  • 线程1检查对象是否存在于数据库中,并发现该对象不存在。
  • 线程1将对象添加到数据库。
  • 对象存在于数据库中。


  • 该对象存在于数据库中。
  • 线程1检查对象是否存在于数据库中,并查看其是否存在。
  • 线程1报告失败,因为该对象存在。
  • 对象仍然存在于数据库中。


  • 该对象在数据库中不存在。
  • 线程1检查对象是否存在于数据库中,并查看该对象是否不存在。
  • 线程2检查对象是否存在于数据库中,并查看该对象是否不存在。
  • 线程1尝试将对象添加到数据库。
  • 线程2尝试将对象添加到数据库。
  • 线程1或线程2成功将对象添加到数据库,而另一个线程由于事务约束而失败。
  • 对象存在于数据库中。

  • 显然,方案一和方案二可以完美地工作,因为只有一个线程在运行。问题出在数字三上。

    问题

    问题是,在情况三中我们进入步骤5时,整个事情都会崩溃。两个线程都有30秒的挂起时间,大多数情况下,两个线程都无法将对象添加到数据库中。

    我知道如何使用全局应用程序锁定轻松解决此问题,但我想知道是否可以在不锁定的情况下解决此问题,从而保留用于数据库访问的异步/等待功能。

    有时,一个线程设法添加对象,另一个线程失败,但是即使那样,两个线程也要花费那30秒才能完成操作,这完全不可用。

    示例输出
    17:41:18|first: Started
    17:41:19|main: Press ENTER
    17:41:19|second: Started
    17:41:20|second: Object does not exist, entering wait ...
    17:41:20|first: Object does not exist, entering wait ...
    17:41:22|first: Wait done
    17:41:22|second: Wait done
    17:41:22|first: Call Insert
    17:41:22|second: Call Insert
    17:41:22|second: Call SaveThrowAsync
    17:41:22|first: Call SaveThrowAsync
    17:41:22|first: Call Commit
    17:41:52|second: Exception: Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while updating the entries. See the inner exception for details. ---> Microsoft.Data.Sqlite.SqliteException: SQLite Error 5: 'database is locked'.
       at Microsoft.Data.Sqlite.Interop.MarshalEx.ThrowExceptionForRC(Int32 rc, Sqlite3Handle db)
       at Microsoft.Data.Sqlite.SqliteCommand.ExecuteReader(CommandBehavior behavior)
       at Microsoft.Data.Sqlite.SqliteCommand.ExecuteReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
       at Microsoft.Data.Sqlite.SqliteCommand.<ExecuteDbDataReaderAsync>d__53.MoveNext()
    --- End of stack trace from previous location where exception was thrown ---
       at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
       at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
       at Microsoft.EntityFrameworkCore.Storage.Internal.RelationalCommand.<ExecuteAsync>d__20.MoveNext()
    --- End of stack trace from previous location where exception was thrown ---
       at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
       at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
       at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.<ExecuteAsync>d__32.MoveNext()
       --- End of inner exception stack trace ---
       at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.<ExecuteAsync>d__32.MoveNext()
    --- End of stack trace from previous location where exception was thrown ---
       at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
       at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
       at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.<ExecuteAsync>d__1.MoveNext()
    --- End of stack trace from previous location where exception was thrown ---
       at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
       at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
       at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.<SaveChangesAsync>d__47.MoveNext()
    --- End of stack trace from previous location where exception was thrown ---
       at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
       at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
       at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.<SaveChangesAsync>d__45.MoveNext()
    --- End of stack trace from previous location where exception was thrown ---
       at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
       at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
       at Microsoft.EntityFrameworkCore.DbContext.<SaveChangesAsync>d__30.MoveNext()
    --- End of stack trace from previous location where exception was thrown ---
       at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
       at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
       at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
       at ConsoleApp1.UnitOfWork.<SaveThrowAsync>d__6.MoveNext() in X:\Dev\NetCore\shit\test1\src\ConsoleApp1\UnitOfWork.cs:line 35
    --- End of stack trace from previous location where exception was thrown ---
       at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
       at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
       at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
       at ConsoleApp1.Program.<ThreadProc>d__2.MoveNext() in X:\Dev\NetCore\shit\test1\src\ConsoleApp1\Program.cs:line 72
    17:41:52|first: Exception: Microsoft.Data.Sqlite.SqliteException: SQLite Error 5: 'database is locked'.
       at Microsoft.Data.Sqlite.Interop.MarshalEx.ThrowExceptionForRC(Int32 rc, Sqlite3Handle db)
       at Microsoft.Data.Sqlite.SqliteCommand.ExecuteReader(CommandBehavior behavior)
       at Microsoft.Data.Sqlite.SqliteCommand.ExecuteNonQuery()
       at Microsoft.Data.Sqlite.SqliteTransaction.Commit()
       at Microsoft.EntityFrameworkCore.Storage.RelationalTransaction.Commit()
       at ConsoleApp1.Program.<ThreadProc>d__2.MoveNext() in X:\Dev\NetCore\shit\test1\src\ConsoleApp1\Program.cs:line 75
    17:41:52|second: Finished
    17:41:52|first: Finished
    17:42:00|main: We have 0 object(s) in the database.
    

    代码

    我试图切断所有与保持最小无关的内容。如果要运行该程序,只需在Visual Studio中创建这些文件,等待.NET Core项目同步,编译该项目,运行“add-migration first”和“update-database”以创建数据库即可运行它。如果没有Visual Studio,则需要使用“dotnet”和“dotnet ef”命令。

    Program.cs:
    using Microsoft.EntityFrameworkCore.Storage;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace ConsoleApp1
    {
      public class Program
      {
        public static void Main(string[] args)
        {
          Thread thread1 = new Thread(new ParameterizedThreadStart(ThreadProc));
          thread1.Start("first");
    
          Thread.Sleep(1000);
    
          Thread thread2 = new Thread(new ParameterizedThreadStart(ThreadProc));
          thread2.Start("second");
    
          log("main", "Press ENTER");
          Console.ReadLine();
    
          using (UnitOfWork uow = new UnitOfWork())
          {
            IEnumerable<DatabaseObject> dbos = uow.DatabaseObjectRepository.GetAsync().Result;
            log("main", "We have {0} object(s) in the database.", dbos.Count());
            foreach (DatabaseObject dbo in dbos)
              log("main", " -> id:{0}, value:{1}", dbo.DatabaseObjectId, dbo.Value);
          }
        }
    
        public static void log(string Id, string Format, params object[] Args)
        {
          string prefix = string.Format("{0}|{1}: ", DateTime.Now.ToString("HH:mm:ss"), Id);
          string msg = string.Format(prefix + Format, Args);
          Console.WriteLine(msg);
        }
    
        public async static void ThreadProc(object State)
        {
          string id = (string)State;
          log(id, "Started", id);
    
          int ourObjectId = 1234;
    
          using (UnitOfWork uow = new UnitOfWork())
          {
            using (IDbContextTransaction transaction = await uow.BeginTransactionAsync())
            {
              bool rollback = false;
              try
              {
                DatabaseObject dbo = (await uow.DatabaseObjectRepository.GetAsync(o => o.DatabaseObjectId == ourObjectId)).FirstOrDefault();
                if (dbo == null)
                {
                  log(id, "Object does not exist, entering wait ...");
                  await Task.Delay(2000); // Same result with Thread.Sleep(2000) instead.
                  log(id, "Wait done");
    
                  dbo = new DatabaseObject()
                  {
                    DatabaseObjectId = ourObjectId,
                    Value = id
                  };
    
                  log(id, "Call Insert");
                  uow.DatabaseObjectRepository.Insert(dbo);
    
                  log(id, "Call SaveThrowAsync");
                  await uow.SaveThrowAsync();
    
                  log(id, "Call Commit");
                  transaction.Commit(); // .NET Core should commit automatically on transaction Dispose, but that does not work for me.
                }
                else
                {
                  log(id, "Object already exists");
                  rollback = true;
                }
              }
              catch (Exception exception)
              {
                log(id, "Exception: {0}", exception.ToString());
              }
    
              if (rollback)
              {
                log(id, "Rolling back");
                transaction.Rollback();
              }
            }
          }
    
          log(id, "Finished");
        }
      }
    }
    

    UnitOfWork.cs:
    using Microsoft.EntityFrameworkCore.Storage;
    using System;
    using System.Threading.Tasks;
    
    namespace ConsoleApp1
    {
      public class UnitOfWork : IDisposable
      {
        private DatabaseContext context = null;
        public DatabaseContext Context
        {
          get
          {
            if (context == null)
              context = new DatabaseContext();
    
            return context;
          }
        }
    
        private GenericRepository<DatabaseObject> databaseObjectRepository;
        public GenericRepository<DatabaseObject> DatabaseObjectRepository
        {
          get
          {
            if (databaseObjectRepository == null)
              databaseObjectRepository = new GenericRepository<DatabaseObject>(Context);
    
            return databaseObjectRepository;
          }
        }
    
        public async Task SaveThrowAsync()
        {
          await Context.SaveChangesAsync();
        }
    
        public async Task<IDbContextTransaction> BeginTransactionAsync()
        {
          return await Context.Database.BeginTransactionAsync();
        }
    
    
        private bool disposed = false;
    
        public void Dispose()
        {
          Dispose(true);
          GC.SuppressFinalize(this);
        }
    
        protected virtual void Dispose(bool Disposing)
        {
          if (disposed) return;
    
          if (Disposing)
          {
            if (context != null) context.Dispose();
            context = null;
            disposed = true;
          }
        }
      }
    }
    

    GenericRepository.cs:
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using System.Linq.Expressions;
    using Microsoft.EntityFrameworkCore;
    
    namespace ConsoleApp1
    {
      public class GenericRepository<TEntity> where TEntity : class
      {
        internal DatabaseContext context;
        internal DbSet<TEntity> dbSet;
    
        public GenericRepository(DatabaseContext context)
        {
          this.context = context;
          dbSet = context.Set<TEntity>();
        }
    
        public virtual async Task<IEnumerable<TEntity>> GetAsync(Expression<Func<TEntity, bool>> filter = null)
        {
          IQueryable<TEntity> query = dbSet;
    
          if (filter != null)
            query = query.Where(filter);
    
          List<TEntity> result = await query.ToListAsync();
          return result;
        }
    
        public virtual void Insert(TEntity entity)
        {
          dbSet.Add(entity);
        }
    
        public virtual void Update(TEntity entityToUpdate)
        {
          dbSet.Attach(entityToUpdate);
          context.Entry(entityToUpdate).State = EntityState.Modified;
        }
      }
    }
    

    DatabaseObject.cs:
    namespace ConsoleApp1
    {
      public class DatabaseObject
      {
        public int DatabaseObjectId { get; set; }
        public string Value { get; set; }
      }
    }
    

    DatabaseContext.cs:
    using Microsoft.EntityFrameworkCore;
    
    namespace ConsoleApp1
    {
      public class DatabaseContext : DbContext
      {
        public DbSet<DatabaseObject> DatabaseObjects { get; set; }
    
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
          optionsBuilder.UseSqlite("Filename=mysqlite.db");
        }
      }
    }
    

    project.json:
    {
      "version": "1.0.0-*",
      "buildOptions": {
        "emitEntryPoint": true
      },
    
      "dependencies": {
        "Microsoft.EntityFrameworkCore.Sqlite": "1.0.0",
        "Microsoft.EntityFrameworkCore.Design": {
          "version": "1.0.0-preview2-final",
          "type": "build"
        },
        "Microsoft.NETCore.App": {
          "version": "1.0.0"
        },
        "System.Runtime.InteropServices": "4.1.0",
        "Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview2-final"
      },
    
      "frameworks": {
        "netcoreapp1.0": {
          "imports": "dnxcore50"
        }
      },
      "tools": {
        "Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview2-final"
      },
      "runtimes": {
        "win10-x64": {}
      }
    }
    

    最佳答案

    EF6SQLite一起使用时,我遇到了类似的问题。这个问题很突出,因为您尝试使用未用于关闭连接的,可用于选择以及更新操作的连接。尝试将本地DbContext using 关键字一起使用。这将在使用后处理dbContext。您至少将避免当前遇到的异常。
    SQLite中的另一个规则是,只有一个连接可以执行写操作。

    因此,我们需要在执行任何其他操作之前确保写入连接已关闭,以使写入可用于其他连接。

    关于c# - 数据库事务中的并发线程会导致.NET Core EF Core的严重延迟,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/39622441/

    10-17 00:43