【问题标题】:How to choose the right concurrency/locking mechanism with Entity Framework如何使用实体框架选择正确的并发/锁定机制
【发布时间】:2017-11-22 07:54:26
【问题描述】:

我正在尝试实现一个简单的服务(使用 C#、SQL Server、实体框架)来处理来自客户的付款,并预先进行几项检查(例如,单一产品一天不能购买超过 10 次)等)

代码的简化版如下:

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 个线程/进程同时开始检查/更新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 秒才能开始限制检查。

任何人都可以为这种情况提出一个好的锁定解决方案吗?

【问题讨论】:

  • 您是否也将付款存储在数据库中?我的意思是一些代表未决/失败/完成支付的支付对象。
  • @Evk,是的,SQL Server是目前各种应用数据唯一的存储机制

标签: c# sql-server entity-framework concurrency


【解决方案1】:

而不是为每个事务使用锁(悲观并发) - 您最好使用乐观并发来检查DailyPaymentCount

使用原始 SQL(因为在 EF 中原子增量很难)- 假定列名:

// 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 &gt; stock,则付款失败。如果支付失败,减少paymentsPending - 这将允许后续支付请求成功。

如 cmets 中所述 - 如果付款失败,则存在统计信息与已处理付款不同步的风险,并且应用程序在 dailyPaymentCount 可以递减之前崩溃。如果这是一个问题(即您无法在应用程序重新启动时重建统计信息) - 您可以使用可在应用程序崩溃时回滚的 RepeatableRead 事务 - 但随后您将回到只能处理同时按 productId 付款,因为产品的 PaymentStatistic 行将在其递增后被锁定 - 直到交易结束。这是不可避免的 - 在您知道自己有库存之前,您无法处理付款,并且在您处理/失败的飞行付款之前,您无法确定是否有库存。

this answer 中对乐观/悲观并发有很好的概述。

【讨论】:

  • 关于“可重复读取事务”的部分有点令人困惑。任何事务隔离级别都会在增量后锁定 PaymentStatistic 行。没有必要为了锁定然后提交/回滚而拥有 RepeatableRead。
  • 正确,但您要确保另一个线程不会读取 PaymentStatistic 行的旧值,并开始处理付款
猜你喜欢
  • 2010-12-10
  • 1970-01-01
  • 2012-04-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-01-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多