问题描述
在高流量场景下,绝对需要确保一次只有一个进程可以修改一块数据。假设你正在为一个极其受欢迎的音乐会构建售票系统。顾客们热切地抢购门票,最后几张票可能同时被售出。如果你不小心,多个顾客可能认为他们已经确保了最后的座位,导致超售!
Entity Framework Core是一个好工具,但它没有直接提供悲观锁的机制。其提供的乐观锁虽然也可以工作,但在高并发场景下,可能导致很多重试。
这里有一个简化的代码片段来说明面临的售票问题:
public async Task Handle(CreateOrderCommand request)
{
await using DbTransaction transaction = await unitOfWork
.BeginTransactionAsync();
Customer customer = await customerRepository.GetAsync(request.CustomerId);
Order order = Order.Create(customer);
Cart cart = await cartService.GetAsync(customer.Id);
foreach (CartItem cartItem in cart.Items)
{
// 哎呀...如果两个请求同时到达这里怎么办?
TicketType ticketType = await ticketTypeRepository.GetAsync(cartItem.TicketTypeId);
ticketType.UpdateQuantity(cartItem.Quantity);
order.AddItem(ticketType, cartItem.Quantity, cartItem.Price);
}
orderRepository.Insert(order);
await unitOfWork.SaveChangesAsync();
await transaction.CommitAsync();
await cartService.ClearAsync(customer.Id);
}
解决方案
上面虚构的示例用来演示这个问题。在结账期间,会验证每张票的剩余数量。如果此时多个并发请求尝试购买同一张票会怎样?最糟糕的情况是“超售”: 并发请求可能会看到有可售的门票,并完成结账。
要靠原生SQL来解决这个问题,EF Core调用原生SQL查询也很容易:
public async Task<TicketType> GetWithLockAsync(Guid id)
{
return await context
.TicketTypes
.FromSql(
$@"SELECT id, event_id, name, price, currency, quantity
FROM ticketing.ticket_types
WITH (UPDLOCK)
WHERE id = {id}") // SQL Server: 锁定或立即失败
.SingleAsync();
}
WITH (UPDLOCK):这是SQL Server中悲观锁的核心。它告诉数据库“锁定这一行,如果它已经被锁定,立即抛出一个错误。”
错误处理
我们将GetWithLockAsync调用包装在try-catch块中,以处理锁定失败,要么重试,要么通知用户。
结语
由于EF Core没有内置的方法添加查询提示,我们通过编写原始SQL查询。使用上述方法进行行级锁定。任何竞争的事务都将失败,直到当前事务释放锁定。这是一个简单的实现悲观锁的方法, 但在实际系统开发中很有用。
标签:Core,await,cartItem,EF,悲观,SQL,id From: https://www.cnblogs.com/odyssey/p/18147541