首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >如何使用Entity Framework选择正确的并发/锁定机制

如何使用Entity Framework选择正确的并发/锁定机制
EN

Stack Overflow用户
提问于 2017-11-22 04:42:53
回答 1查看 438关注 0票数 2

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

代码的简化版本如下:

代码语言:javascript
运行
复制
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();
    }
}

我担心的是可能的并发问题。我需要确保没有两个线程/进程同时开始检查/更新stats.PaymentCount,否则统计数据将不同步。

我在考虑将整个方法包装成一个分布式锁(例如使用this implementation),如下所示:

代码语言:javascript
运行
复制
string lockKey = $"processing-payment-for-product-{productId}";
var myLock = new SqlDistributedLock(lockKey);
using (myLock.Acquire())
{
    ExecutePayment(productId, paymentInfo);
}

但这种方法的问题是ProcessPayment非常慢(2-3秒),这意味着同一产品的任何并发付款请求都必须等待2-3秒才能开始限额检查。

对于这种情况,有没有人能推荐一个好的锁定解决方案?

EN

回答 1

Stack Overflow用户

发布于 2017-11-22 06:25:02

与其为每个事务使用锁(悲观并发),不如使用乐观并发来检查DailyPaymentCount

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

代码语言:javascript
运行
复制
// 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事务-但随后您只能同时处理每个productId的付款,因为产品的PaymentStatistic行在其增加后将被锁定-直到事务结束。这在某种程度上是不可避免的-你不能处理付款,直到你知道你有库存,而且你不能确定你是否有库存,直到你处理了/失败了飞行中的付款。

this answer中有一个关于乐观/悲观并发的很好的概述。

票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/47422352

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档