DBトランザクションの同時スレッドで.NETコアEFコアの遅延が大きくなる

.net-core c# database entity-framework-core multithreading

質問

バックグラウンド

SQLite DBで動作する非同期サーバーを作成しようとしています。私はEntity Frameworkコアで .NETコアを使用しています。

私はGenericRepositoryパターンでUnitOfWorkを使用していますが、下のサンプルコードで示すように、これは実際にはこれらのパターンとは関係ありません。

私はWindows 10を使用していますが、サポートされている.NET Coreプラットフォームでも同様の動作が期待されます。

私が達成したいこと

私が達成したいのは、合理的な取引行動です。私は問題全体を単純なシナリオに減らしました。特定のオブジェクトが存在する場合はデータベースを調べ、そうでない場合は追加します。それ以外の場合は失敗します。この全体的な操作はトランザクション内にあり、予想されるシナリオは次のとおりです。

1

  1. オブジェクトはデータベースに存在しません。
  2. スレッド1は、オブジェクトがデータベースに存在するかどうかをチェックし、存在しないと判断します。
  3. スレッド1はオブジェクトをデータベースに追加します。
  4. オブジェクトがデータベースに存在します。

  1. オブジェクトはデータベースに存在します。
  2. スレッド1は、オブジェクトがデータベースに存在するかどうかをチェックし、オブジェクトが存在するかどうかを確認します。
  3. スレッド1はオブジェクトが存在するために失敗を報告します。
  4. オブジェクトはまだデータベースに存在します。

  1. オブジェクトはデータベースに存在しません。
  2. スレッド1は、オブジェクトがデータベースに存在するかどうかをチェックし、オブジェクトが存在しないことを確認します。
  3. スレッド2は、オブジェクトがデータベースに存在するかどうかをチェックし、オブジェクトが存在しないことを確認します。
  4. スレッド1はオブジェクトをデータベースに追加しようとします。
  5. スレッド2はオブジェクトをデータベースに追加しようとします。
  6. スレッド1またはスレッド2のいずれかがオブジェクトをデータベースに追加すると、トランザクションの制約のためにもう一方のスレッドが失敗します。
  7. オブジェクトがデータベースに存在します。

明らかに、シナリオ1と2は完全に動作します。なぜなら、単一のスレッドしか動作していないからです。問題は3番です。

問題

問題は、状況3でステップ5に入ると、すべてが爆発するということです。両方のスレッドで30秒のハングがあり、どちらのスレッドもオブジェクトをデータベースに追加することはできません。

グローバルアプリケーションロックでこれを簡単に解決する方法はわかっていますが、ロックせずにこの問題を解決できるかどうかを知りたいので、データベースアクセスの非同期/待機機能を保持しておきたいと思います。

1つのスレッドがオブジェクトの追加を管理し、もう1つのスレッドが失敗する場合もありますが、両方のスレッドが操作を完了するまでに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コアプロジェクトの同期が完了するまで待ってから、プロジェクトをコンパイルしてから「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で同じような問題に直面していました。この問題は、接続のクローズを行わずに、選択操作と更新操作で再利用される接続を使用しようとしているために顕著です。キーワードを使用してローカルDbContextusingusingみてください。これは、使用後にdbContextします。あなたは少なくともあなたが現在取得している例外を避けるでしょう。

SQLiteのもう1つのルールは、書き込み操作を実行できる接続は1つだけです。

したがって、書き込みを他の接続で利用できるようにするには、他の操作を行う前に書き込み接続が閉じられていることを確認する必要があります。



Related

ライセンスを受けた: CC-BY-SA with attribution
所属していない Stack Overflow
このKBは合法ですか? はい、理由を学ぶ
ライセンスを受けた: CC-BY-SA with attribution
所属していない Stack Overflow
このKBは合法ですか? はい、理由を学ぶ