2017-11-21 2 views
2

クライアントからの支払いを処理する単純なサービス(C#、SQL Server、Entity Frameworkを使用)を実装しようとしています。単一の製品は、など、10回以上の日を購入することはできません)Entity Frameworkで正しい並行性/ロックメカニズムを選択する方法

コードの簡易版は、以下の通りです:私は心配です何

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(); 
    } 
} 

可能同時実行の問題です。 2つのスレッド/プロセスが同時にチェック/更新を開始しないようにする必要があります。そうしないと、統計情報が同期しなくなります。

私はこのような(this implementationを使用して、たとえば)分散ロックにメソッド全体を包む考えていた:

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

しかし、このアプローチの懸念はProcessPaymentが非常に遅いことである(2-3秒)これは、同じ製品の同時支払い要求が、制限チェックが開始されるまで2〜3秒待たなければならないことを意味します。

誰もがこのケースで良いロックソリューションを提案できますか?

+0

また、データベースに支払いを保存していますか?保留中/失敗/完全な支払いを表す支払いオブジェクトを意味します。 – Evk

+0

@Evk、はい、現在のところ、SQL Serverはすべての種類のアプリケーションデータの唯一の格納メカニズムです –

答えて

0

トランザクションごとにロックを使用するのではなく(Pessimistic Concurrency)、オプティミスティック並行性を使用してDailyPaymentCountをチェックする方がよい場合があります。

(原子単位はEFでハードなので)生SQLを使用して - 列名が仮定:

これは効果的にあなたの統計では、特定の製品のための「飛行で 」の支払いを含めている
// 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)); 
      } 
     } 
    } 
} 

- とすることを使用して障壁として。お支払いを処理する前に - あなたの統計情報を(アトミックに)増やそうとします。productsSold + paymentsPending > stockの場合、支払いが失敗します。支払いが失敗した場合は、paymentsPendingを減額します。これにより、後続の支払い要求が成功することができます。

コメントに記載されているように、支払いが失敗した場合、統計情報が処理済みの支払いと同期していない可能性があり、dailyPaymentCountを減算する前にアプリケーションがクラッシュします。これが問題の場合(つまりアプリケーションの再起動時に統計を再構築することはできません)、アプリケーションのクラッシュ時にロールバックされるRepeatableReadトランザクションを使用できますが、商品のPaymentStatistic行がインクリメントされた後、トランザクションの終了までロックされるため、商品ごとの支払いが同時に行われます。これはやむを得ないことです。在庫があることを知るまで支払いを処理することはできず、機内支払いを処理/失敗するまで在庫があるかどうかはわかりません。

this answerに楽観的/悲観的な並行処理の概要があります。

+0

"RepeatableRead transaction"に関するビットは多少混乱します。すべてのトランザクション分離レベルは、増分後にPaymentStatistic行をロックします。ロックし、次にコミット/ロールバックするためにRepeatableReadを持つ必要はありません。 –

+0

正解ですが、別のスレッドがPaymentStatistic行の古い値を読み取っていないことを確認し、支払い処理を開始したい – georgevanburgh

関連する問題