Hintergrund
Ich versuche, einen asynchronen Server zu schreiben, der mit SQLite DB funktioniert. Ich verwende .NET Core mit Entity Framework Core .
Ich verwende UnitOfWork mit GenericRepository-Mustern, aber wie mein Beispielcode unten zeigt, hängt dies nicht wirklich mit diesen Mustern zusammen.
Ich verwende Windows 10, aber ich würde erwarten, dass sich jede unterstützte .NET Core-Plattform genauso verhält.
Was ich erreichen möchte
Was ich erreichen möchte, ist ein vernünftiges Transaktionsverhalten. Ich habe das ganze Problem auf ein einfaches Szenario reduziert, in dem ich in die Datenbank schaue, ob ein bestimmtes Objekt existiert und wenn nicht, füge ich es hinzu, sonst scheitere ich. Diese gesamte Operation befindet sich in einer Transaktion und die erwarteten Szenarien sind:
Ein
Zwei
Drei
Offensichtlich funktionieren die Szenarios Eins und Zwei perfekt, weil nur ein einziger Thread läuft. Das Problem ist mit der Nummer Drei.
Das Problem
Das Problem ist, dass wenn wir in Situation Drei in Schritt 5 eintreten, das ganze Ding in die Luft geht. Es gibt eine 30-Sekunden-Verzögerung für beide Threads und meistens gelingt es keinem der Threads, das Objekt zur Datenbank hinzuzufügen.
Ich weiß, wie man dies leicht mit einer globalen Anwendungssperre lösen kann, aber ich würde gerne wissen, ob es möglich ist, dies ohne Sperren zu lösen und somit die asynchrone / Wartefunktion für den Datenbankzugriff zu erhalten.
Manchmal schafft es ein Thread, das Objekt hinzuzufügen, der andere Thread schlägt fehl, aber selbst dann dauert es 30 Sekunden, bis beide Threads die Operation abgeschlossen haben, was völlig unbrauchbar ist.
Beispielausgabe
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.
Code
Ich habe versucht, alles abzuschneiden, was nicht damit zusammenhängt, um das minimal zu halten. Wenn Sie das Programm ausführen möchten, erstellen Sie diese Dateien in Visual Studio, warten Sie auf die .NET Core-Projektsynchronisierung, kompilieren Sie das Projekt, führen Sie "add-migration first" und "update-database" aus, um die Datenbank zu erstellen . Ohne Visual Studio müssen Sie die Befehle "dotnet" und "dotnet ef" verwenden.
Programm.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": {}
}
}
Ich sah ähnliches Problem während der Verwendung mit EF6
und SQLite
. Das Problem tritt auf, weil Sie versuchen, eine Verbindung zu verwenden, die für Auswahl- und Aktualisierungsvorgänge wiederverwendet wird, ohne dass die Verbindung geschlossen wird. Versuchen Sie lokale verwenden DbContext
mit using
Stichwort. Dadurch wird der dbContext
nach seiner Verwendung entfernt. Sie werden zumindest die Ausnahme vermeiden, die Sie gerade bekommen.
Eine andere Regel in SQLite
ist, dass nur eine Verbindung den Schreibvorgang ausführen kann.
Daher müssen wir sicherstellen, dass die Schreibverbindung geschlossen ist, bevor irgendeine andere Operation ausgeführt wird, um den Schreibvorgang für andere Verbindungen verfügbar zu machen.