2017-11-21 13 views
2

클라이언트에서 지불을 처리하는 간단한 서비스 (예 : C#, SQL Server, Entity Framework)를 구현하려고합니다. 예를 들어, 나는 가능한 동시성 문제입니다 대해 걱정 무엇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(); 
    } 
} 

: 하나의 제품 이상 10 회 등)을 구입할 수 없습니다

코드의 단순화 된 버전은 다음과 같다. 두 스레드/프로세스가 동시에 확인/업데이트를 시작하는지 확인해야합니다. 그렇지 않으면 통계가 동기화되지 않습니다. stats.PaymentCount

이 같은 (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

각 트랜잭션마다 잠금을 사용하는 대신 (비관적 동시성) - 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