在 ASP.NET Core API 中处理并发礼品卡购买而不导致数据库并发问题

问题描述 投票:0回答:1

我正在开发一个礼品卡系统,其中有两个主要数据库表:
礼品卡激活代码。每张礼品卡可以有许多预先生成的激活码,用户购买礼品卡后,他们应该会收到可用的激活码之一。

// ActivationCode Entity
public class ActivationCode : BaseEntity
{
    public Guid Id { get; set; }
    public string Code { get; set; }
    public bool isUsed { get; set; } = false;
    public DateTime? ExpiresOn { get; set; }
    public bool IsExpired => DateTime.UtcNow >= ExpiresOn;

    public Guid GiftCardId { get; set; }
    public GiftCard GiftCard { get; set; }
    
    [Timestamp]
    public uint RowVersion { get; set; } // For Optimistic Concurrency Control
}

// GiftCard Entity
public class GiftCard : BaseEntity
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public decimal Value { get; set; }

    public List<ActivationCode> ActivationCodes { get; set; }
}

//Purchase Gift Card

public async Task<GeneralResult<GiftCardPurchaseResult>> PurchaseGiftCard(Guid giftCardId, string userId)
    {
        // 1. Retrieve the gift card information from the database (checks if it's valid and available).
    
        // 2. Attempt to get an available activation code that hasn't been used or expired, and isn't deleted.
        var getAvailableActivationCode = await _activationCodeRepository
            .GetTableNoTracking()
            .Where(gd => gd.GiftCardId == giftCardId && !gd.isUsed && !gd.IsDeleted && 
                         (DateTime.UtcNow < gd.ExpiresOn || gd.ExpiresOn == null))
            .FirstOrDefaultAsync();
    
        // If no activation codes are available, return an error indicating the gift card is out of stock.
        if (getAvailableActivationCode is null)
        {
            return GeneralResult<GiftCardPurchaseResult>
                .Failure("No activation codes are currently available for this gift card.",
                HttpStatusCode.BadRequest,
                new List<string> { GiftCardTransactionsErrorCode.ACTIVATION_CODES_NOT_AVAILABLE });
        }
    
        // 3. Retrieve the user's wallet balance and check if the balance is sufficient for the purchase.
    
    
        using (var transaction = await _dataContext.Database.BeginTransactionAsync())
        {
            try
            {
                // 4. Debit the user's wallet by deducting the gift card price.
     
                // 5. Mark the activation code as "used" by setting isUsed to true.
                getAvailableActivationCode.isUsed = true;
    
                // Update the activation code record in the database.
                await _activationCodeRepository.UpdateAsync(getAvailableActivationCode);
    
                // 6. Create a new entry in the GiftCardPurchases table to record the purchase details.
    
                // Commit the transaction to apply changes to the database.
                await transaction.CommitAsync();
    
                // 7. Return a successful result with the purchase information.
                return GeneralResult<GiftCardPurchaseResult>.Success(purchaseResultInfo, "Gift card purchased successfully.");
            }
            catch (DbUpdateConcurrencyException ex)
            {
                // Handle concurrency issues (e.g., another transaction might have updated the same activation code).
            }
        }
    }

在我的 API(Asp.net core Web api)中,当用户购买礼品卡时,他们会被分配一个可用的、未使用的激活码(isUsed == false 和 !IsExpired)。当对同一张礼品卡的并发请求导致多个用户被分配相同的激活码时,就会出现问题。由于我的乐观并发控制设置(RowVersion),这会导致 DBConcurrencyException。

主要挑战是系统不需要等待特定的激活码被释放,因为还有其他可用的代码。但是,我遇到了尝试修改相同激活码的并发事务的问题。

我尝试过的:

悲观锁定:这在我的情况下并不理想,因为还有其他可用的激活码,并且我们不希望系统不必要地等待特定行被解锁。

乐观并发控制:我使用 RowVersion 字段实现了乐观并发。但是,当两个用户尝试选择并将相同的激活码标记为已使用时,第二个事务将失败并出现 DbUpdateConcurrencyException。这会强制重试,但我宁愿通过确保预先选择不同的代码来完全避免这些并发冲突。

问题: 如何确保当并发用户购买同一张礼品卡时,他们始终分配到不同的可用激活码,而不会遇到数据库并发问题?是否有更好的模式来处理有多个激活码可用的情况?

任何有关处理此类并发问题的模式或方法的见解将不胜感激!

c# postgresql asp.net-web-api concurrency entity-framework-core
1个回答
0
投票

我会使用 Polly nuget 包进行重试。

定义将在

DbUpdateConcurrencyException
上重试操作的策略:

private readonly RetryPolicy _policy = Policy
           .Handle<DbUpdateConcurrencyException>()
           .WaitAndRetry(3, _ => TimeSpan.FromSeconds(1));

并在您的方法中使用它:

public async Task<GeneralResult<GiftCardPurchaseResult>> PurchaseGiftCard(Guid giftCardId, string userId)
{
    return await _policy.Execute(async () => 
    {
        ... your code
    });
}
© www.soinside.com 2019 - 2024. All rights reserved.