信号量Slim vs Hangfire

SemaphoreSlim vs Hangfire

提问人:Josh 提问时间:11/14/2023 最后编辑:Josh 更新时间:11/14/2023 访问量:71

问:

我有一个解决方案,可以接收处理订单的请求。条件是,如果订单状态为“已付款”,则处理订单。其他订单状态包括“已发货”和“待处理”。

示例代码在这里

var orderDetails = await _dbContext.Orders.FirstOrDefaultAsync(s => s.Id == orderId);

string errMsg = string.Empty;

if (orderDetails == null)
{
errMsg = "Invalid order";
//Handle invalid order id case
return new ServiceTypeResponse<SimpleOrderDetailsResponseModel>
  {
    Data = null,
    Errors = new List<string> { errMsg },
    ResponseMessage = errMsg,
    StatusCode = 400,
    Success = false
  };
}

if (orderDetails.Status == OrderStatus.Fullfilled)
{

//handle already fulfilled order case
errMsg = "This order has already been fulfilled";
return new ServiceTypeResponse<SimpleOrderDetailsResponseModel>
{
    Data = new SimpleOrderDetailsResponseModel(orderDetails),
    Errors = null,
    ResponseMessage = errMsg,
    StatusCode = 200,
    Success = true
};
}

if (orderDetails.Status == OrderStatus.Paid)
{
 //1. process the order here and give value to the customer
 //2. Change the status of the order to Fulfilled
}
return;
}

我试图避免多个线程向此解决方案发送请求以处理订单的争用条件。 为了避免从多个线程中多次处理订单,我正在考虑实现 SemaphoreSlim 或使用 Hangfire 对请求进行排队。

尽管有人在评论中建议在数据库上使用乐观并发锁定,但在这种情况下,乐观并发的问题在于乐观并发不会阻止记录被另一个线程读取。因此,在一个线程从数据库中读取记录并将订单状态读取为“已付款”后,在处理订单并可能调用第三方服务来履行订单之前,在将记录更新为“已履行”之前,另一个线程不妨读取记录并调用第三方服务并第二次履行订单,然后再尝试更新记录。我这个cae,虽然第二个线程无法更新订单的状态,但它会完成订单两次。

考虑到性能和最佳实践,避免多次处理订单的最佳方法是什么

问候

C# 多线程 ASP.NET-Core 信号量 hangfire

评论

3赞 Stephen Cleary 11/14/2023
也不。对数据库使用开放式并发。让数据库成为唯一的事实来源。
0赞 Josh 11/14/2023
在这种情况下,乐观并发的@StephenCleary问题在于,在通过一个线程从数据库中读取记录并将订单状态读取为“已支付”之后,同时仍在处理订单并可能调用第三方服务来履行订单,在将记录更新为“已履行”之前,另一个线程还不如读取记录并调用第三方服务并在尝试之前第二次履行订单以更新记录。我这个cae,虽然第二个线程无法更新订单的状态,但它会两次完成订单
4赞 Magnus 11/14/2023
在开始大作业之前,当状态为“已支付”时,可以在交易中将状态设置为“正在处理”。
0赞 Josh 11/14/2023
@Magnus这听起来是一个很好的解决方案。我在这里看到的唯一缺点是,它为该过程增加了一次数据库访问的开销。同样,在乐观并发中,如果记录列的值已为“Processing”,而另一个线程尝试将记录更新为相同的值“Processing”,则该过程的结果是什么?
1赞 JonasH 11/14/2023
确保对第三方服务的调用只发生一次是不可能的。您可以按照 Magnus 的建议设置一个“处理”标志,但电源可能会在之后消失,让您不确定是否进行了调用。解决方案是确保调用是幂等的,以便多个相同的调用不会产生有害影响,或者放宽您提供的保证。但是,再多的锁/信号量等也无法保护您免于断电。

答:

1赞 Magnus 11/14/2023 #1

在具有所需隔离级别的事务中,运行以下命令(而不是 ):_dbContext.Orders.FirstOrDefaultAsync(...)

UPDATE orders
SET Status = 'Processing'
OUTPUT deleted.*
WHERE id = @orderId AND Status = 'Paid'

deleted.*将返回更新前的实体,并将具有 status ,并将其映射到您的类。然后继续执行其余代码。如果指定的订单没有状态已支付,则返回为止的空集。PaidorderDetails