2017-11-21 17 views
2

Ich versuche, einen einfachen Service zu implementieren (C#, SQL Server, Entity Framework), die vorher (zum Beispiel geschehen, Zahlungen von Kunden mit mehreren Check verarbeiten würden, ein einziges Produkt kann mehr als 10 mal am Tag nicht gekauft werden, etc.)Wie Sie den richtige Gleichzeitigkeit/Verriegelungsmechanismus mit Entity Framework wählen

die vereinfachte Version des Codes ist die folgende:

public void ExecutePayment(int productId, PaymentInfo paymentInfo) 
{ 
    using (var dbContext = new MyDbContext()) 
    { 
     var stats = dbContext.PaymentStatistics.Single(s => s.ProductId== productId); 
     var limits = dbContext.Limits.Single(l => l.ProductId == productId); 
     int newPaymentCount = stats.DailyPaymentCount + 1; 
     if (newPaymentCount > limits.MaxDailyPaymentCount) 
     { 
      throw new InvalidOperationException("Exceeded payment count limit"); 
     } 

     // other limits here... 

     var paymentResult = ProcessPayment(paymentInfo); <-- long operation, takes 2-3 seconds 
     if (paymentResult.Success) 
     { 
      stats.DailyPaymentCount = newPaymentCount; 
     } 

     dbContext.SaveChanges(); 
    } 
} 

Was mich betrifft mögliche Parallelitätsprobleme sind. Ich muss sicherstellen, dass keine 2 Threads/Prozesse gleichzeitig stats.PaymentCount prüfen/aktualisieren, sonst sind die Statistiken nicht mehr synchron.

Ich dachte über das gesamte Verfahren in eine Distributed Lock (zB this implementation verwenden) Umwickeln wie folgt aus:

string lockKey = $"processing-payment-for-product-{productId}"; 
var myLock = new SqlDistributedLock(lockKey); 
using (myLock.Acquire()) 
{ 
    ExecutePayment(productId, paymentInfo); 
} 

Aber die Beschäftigung mit diesem Ansatz ist, dass ProcessPayment sehr langsam (2-3 Sekunden) Dies bedeutet, dass alle gleichzeitigen Zahlungsanforderungen für dasselbe Produkt 2-3 Sekunden warten müssen, bevor die Limitprüfungen überhaupt beginnen können.

Kann mir jemand eine gute Sperr Lösung für diesen Fall vorschlagen?

+0

Haben Sie auch Zahlungen in Datenbank speichern? Ich meine ein Zahlungsobjekt, das eine ausstehende/fehlgeschlagene/abgeschlossene Zahlung darstellt. – Evk

+0

@Evk, ja, ist SQL Server derzeit der einzige Speichermechanismus für alle Arten von Anwendungsdaten –

Antwort

0

Anstatt für jede Transaktion eine Sperre zu verwenden (Pessimistische Parallelität), ist die Verwendung von Optimistischer Concurrency für die Überprüfung besser geeignet. DailyPaymentCount.

Verwendung von Raw SQL (seit Atom-Schritten nur schwer in EF) - Spaltennamen angenommen:

// Atomically increment dailyPaymentCount. Fail if we're over the limit. 
private string incrementQuery = @"UPDATE PaymentStatistics p 
       SET dailyPaymentCount = dailyPaymentCount + 1 
       FROM PaymentStatistics p 
       JOIN Limits l on p.productId = l.productId 
       WHERE p.dailyPaymentCount < l.maxDailyPaymentCount 
       AND p.productId = @givenProductId"; 

// Atomically decrement dailyPaymentCount 
private string decrementQuery = @"UPDATE PaymentStatistics p 
       SET dailyPaymentCount = dailyPaymentCount - 1 
       FROM PaymentStatistics p 
       WHERE p.productId = @givenProductId"; 

public void ExecutePayment(int productId, PaymentInfo paymentInfo) 
{ 
    using (var dbContext = MyDbContext()) { 

     using (var dbContext = new MyDbContext()) 
     { 
      // Try to increment the payment statistics for the given product 
      var rowsUpdated = dbContext.Database.ExecuteSqlCommand(incrementQuery, new SqlParameter("@givenProductId", productId)); 

      if (rowsUpdated == 0) // If no rows were updated - we're out of stock (or the product/limit doesn't exist) 
       throw new InvalidOperationException("Out of stock!"); 

      // Note: there's a risk of our stats being out of sync if the program crashes after this point 
      var paymentResult = ProcessPayment(paymentInfo); // long operation, takes 2-3 seconds 

      if (!paymentResult.Success) 
      { 
       dbContext.Database.ExecuteSqlCommand(decrementQuery, new SqlParameter("@givenProductId", productId)); 
      } 
     } 
    } 
} 

Diese effektiv ist wie „im Flug“ Zahlungen für ein bestimmtes Produkt in Ihren Statistiken - und die Verwendung dieser als Barriere. Bevor Sie eine Zahlung verarbeiten, versuchen Sie (atomar) Ihre Statistiken zu erhöhen - und die Zahlung fehlschlagen, wenn productsSold + paymentsPending > stock. Wenn eine Zahlung fehlschlägt, dekrementieren Sie paymentsPending, damit nachfolgende Zahlungsanforderungen erfolgreich ausgeführt werden können.

Wie in den Kommentaren erwähnt - besteht die Gefahr, dass die Statistiken nicht mit den verarbeiteten Zahlungen übereinstimmen, wenn die Zahlung fehlschlägt und die Anwendung abstürzt, bevor dailyPaymentCount dekrementiert werden kann. Wenn dies ein Problem ist (dh Sie können keine Statistiken zum Neustart der Anwendung erstellen), können Sie eine RepeatableRead-Transaktion verwenden, die bei einem Anwendungsabsturz zurückgesetzt wird. Danach können Sie jedoch nur noch verarbeiten Zahlung pro productId gleichzeitig, da die PaymentStatistic-Zeile für ein Produkt nach dem Inkrementieren gesperrt wird - bis zum Ende der Transaktion. Dies ist unvermeidlich - Sie können Zahlungen erst verarbeiten, wenn Sie wissen, dass Sie Aktien haben, und Sie werden nicht sicher wissen, ob Sie Aktien haben oder nicht, bis Sie die Zahlungen im Flug bearbeitet haben.

Es gibt einen guten Überblick über optimistisch/pessimistisch Concurrency in this answer.

+0

das Bit über „eine Repeatable Transaktion“ etwas verwirrend ist. Jede Transaktionsisolationsstufe sperrt die PaymentStatistic-Zeile nach dem Inkrement. Es ist nicht notwendig, RepeatableRead zu haben, um zu sperren und dann zu committen/rollback. –

+0

Korrigieren Sie, aber Sie möchten sicherstellen, dass ein anderer Thread den alten Wert der PaymentStatistic-Zeile nicht liest und mit der Verarbeitung der Zahlung beginnt – georgevanburgh

Verwandte Themen