我正在尝试实现一个简单的服务(使用C#、SQL Server、Entity Framework),它将处理来自客户的付款,并预先进行几次检查(例如,单个产品每天不能购买超过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();
}
}
我担心的是可能的并发问题。我需要确保没有两个线程/进程同时开始检查/更新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秒才能开始限额检查。
对于这种情况,有没有人能推荐一个好的锁定解决方案?
发布于 2017-11-22 06:25:02
与其为每个事务使用锁(悲观并发),不如使用乐观并发来检查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 > stock
,则无法完成付款。如果支付失败,则减少paymentsPending
-这将允许后续支付请求成功。
正如评论中提到的-如果支付失败,统计数据可能会与已处理的支付不同步,并且应用程序在dailyPaymentCount可以递减之前崩溃。如果这是一个问题(即您不能在应用程序重新启动时重建统计数据)-您可以使用在应用程序崩溃的情况下回滚的RepeatableRead事务-但随后您只能同时处理每个productId的付款,因为产品的PaymentStatistic行在其增加后将被锁定-直到事务结束。这在某种程度上是不可避免的-你不能处理付款,直到你知道你有库存,而且你不能确定你是否有库存,直到你处理了/失败了飞行中的付款。
在this answer中有一个关于乐观/悲观并发的很好的概述。
https://stackoverflow.com/questions/47422352
复制相似问题