Laden...

Mit EF Core Concurrency auf Datenbankebene umsetzen?

Erstellt von BlackMatrix vor 5 Jahren Letzter Beitrag vor 5 Jahren 1.609 Views
B
BlackMatrix Themenstarter:in
218 Beiträge seit 2012
vor 5 Jahren
Mit EF Core Concurrency auf Datenbankebene umsetzen?

verwendetes Datenbanksystem: EF Core 2.1

Ich möchte gerne meine Tabelle, auf die mehrere Prozesse zugreifen können, durch einen Concurrency Mechanismus erweitern.

Ich frage mich gerade, ob ich für das folgende Szenario zwei Spalten benötige oder ob das auch mit einer gelöst werden kann.

  1. Es sollen Daten aus der Tabelle geholt werden, die beim Abrufen mit einem "in Bearbeitung Zeitstempel" markiert werden sollen.
  2. Dann folgt die Bearbeitung der Daten.
  3. Nach Abschluss wird dieser Zeitstempel zurückgesetzt.

Kann ich jetzt an dieser einen Spalte den Bearbeitungszeitpunkt + optimistischen Concurrency Check festmachen, wenn 1. in einer Transaktion stattfindet oder brauche ich dafür eine weitere Spalte?
Ein Service ist zwar dazwischengeschaltet, aber sollte man das nicht eher auf Datenbankebene abhandeln, falls dieser mal irgendwie abstürzt?

Viele Grüße

16.835 Beiträge seit 2008
vor 5 Jahren

EF Core hat bereits eine Concurrency-Implementierung.
Du hast EF Core: Behandlung von Parallelitätskonflikten bei Deiner Recherche bereits gesehen?

Für Optimistic Currency braucht man keine Änderung am Schema.

B
BlackMatrix Themenstarter:in
218 Beiträge seit 2012
vor 5 Jahren

Die Seite ist mir bekannt.

Ich hätte jetzt eigentlich nicht vermutet, dass ich die Spalte, die für den Start des Bearbeitungszeitpunkts ist, zusätzlich auch noch als Concurrency Token verwenden kann. Ich bin davon ausgegangen, dass zunächst alle SQL Statements abgefeuert werden und am Ende mit einem Select-Statement gegengeprüft wird, ob die Spalte noch den Wert hat, den es am Beginn der Transaktion ausgelesen hat. Es wird aber ein Select-Statement abgefeuert und nur im Update-Statement gegengeprüft. Ich frage mich, ob das dann threadsafe ist?


SELECT "x"."Id", "x"."ProcessingDateTime"
FROM "Entities" AS "x"
WHERE "x"."ProcessingDateTime" = '0001-01-01 00:00:00'
LIMIT 1


UPDATE "Entities" SET "ProcessingDateTime" = '2018-11-03 22:23:34'
WHERE "Id" = 1 AND "ProcessingDateTime" = '0001-01-01 00:00:00';
SELECT changes();

Leider konnte ich es keine DbUpdateConcurrencyException provozieren, vermutlich weil SQLite Lockingmechanismen auf Dateiebene vornimmt. Aber wäre das so korrekt? Den Transaktionscope bei dem Save kann ich ja weglassen, weil nur eine Operation, oder?


namespace ConsoleApp
{
    internal class Program
    {
        private static readonly Logger Log = LogManager.GetCurrentClassLogger();

        private static void Main(string[] args)
        {
            Log.Info("Start...");

            const int threadCount = 100;

            var threads = Enumerable.Range(0, threadCount).Select(_ => new Thread(async () =>
            {
                while (true)
                {
                    var entity = await GetAsync();
                    if (entity != null)
                    {
                        await ProcessAsync(entity);

                        await SaveAsync(entity);
                    }
                }
            }));

            foreach (var thread in threads)
            {
                thread.Start();
            }

            Console.ReadKey();
        }

        private static async Task SaveAsync(Entity entity)
        {
            using (var context = new DataContext())
            {
                context.Entities.Attach(entity);

                entity.ProcessingDateTime = DateTime.MinValue;

                await context.SaveChangesAsync();
            }
        }

        private static async Task ProcessAsync(Entity entity)
        {
            // Processing...
        }

        private static async Task<Entity> GetAsync()
        {
            // Entered by multiple threads...

            using (var context = new DataContext())
            {
                using (var transaction = await context.Database.BeginTransactionAsync())
                {
                    while (true)
                    {
                        try
                        {
                            var entity = await context.Entities.FirstOrDefaultAsync(x => x.ProcessingDateTime == DateTime.MinValue);
                            if (entity == null)
                            {
                                return null;
                            }

                            entity.ProcessingDateTime = DateTime.Now;

                            await context.SaveChangesAsync();

                            transaction.Commit();

                            return entity;
                        }
                        catch (DbUpdateConcurrencyException)
                        {

                        }
                    }
                }
            }
        }

        public class DataContext : DbContext
        {
            public DbSet<Entity> Entities { get; set; }

            protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            {
                optionsBuilder.UseSqlite("Data Source=data.db");
            }

            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                base.OnModelCreating(modelBuilder);

                modelBuilder
                    .Entity<Entity>()
                    .Property(x => x.ProcessingDateTime)
                    .IsConcurrencyToken();
            }
        }

        public class Entity
        {
            public int Id { get; set; }
            public DateTime ProcessingDateTime { get; set; }
        }
    }
}
W
955 Beiträge seit 2010
vor 5 Jahren

Und warum verwendest Du den eingebauten Mechanismus nicht? Du schreibst von einem optimistischen Sperrkonzept und verwendest aber die pessimistischen Sperren. Das sind zwei unterschiedliche Dinge. Ebenfalls musst du dich mit den einzelnen Isolationsstufen der Transaktionen beschäftigen und dem Fakt dass Lesen und Schreiben im EF durch unterschiedliche Transaktionen bewerkstelligt werden.
Wenn du die Sperrfähigkeit testen willst kannst du doch einfach den Client zwei mal starten: Du liest bei beiden dieselben Daten ein änderst beide und speicherst sie dann. Dieses Threadgeraffel enthält vielleicht Fehler die das Verhalten nicht aufzeigen.

B
BlackMatrix Themenstarter:in
218 Beiträge seit 2012
vor 5 Jahren

Wie meinst du das, ich verwende den eingebauten Mechanismus nicht? Ich muss sicherstellen, dass während die Entität verarbeitet wird

ProcessAsync

sie von keinem anderen Prozess auch verarbeitet wird. Und falls die Bearbeitung aus irgendeinem Grund hart abbricht, kann ich an dieser Bearbeitungszeit festmachen, dass ich sie nach einer gewissen Zeit wieder entsperren kann. Wenn nun die Entität aus der DB geholt wird, sollte das aber auch immer nur einer tun, daher hab ich ProcessingDateTime auch als IsConcurrencyToken definiert.

16.835 Beiträge seit 2008
vor 5 Jahren

Du bist lang genug im Forum dabei; daher unterlasse bitte (endlich) die Full Quotes. Danke.
[Hinweis] Wie poste ich richtig?

Wie gesagt; für nen Optimistic Concurrency braucht man keine Hilfsspalten; da gibt es an für sich auch kein "Sperren bei Abbruch".
Es riecht so, als ob Du Pessimistic Concurrency willst (jedenfalls hört sich das so an) - das ist aber um Welten komplexer.

Magst Dich evtl. nochmal mit beiden Konzepten befassen und dann entscheiden, was Du möchtest?