我正在开发一个礼品卡系统,其中有两个主要数据库表:
礼品卡和激活代码。每张礼品卡可以有许多预先生成的激活码,用户购买礼品卡后,他们应该会收到可用的激活码之一。
// 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。这会强制重试,但我宁愿通过确保预先选择不同的代码来完全避免这些并发冲突。
问题: 如何确保当并发用户购买同一张礼品卡时,他们始终分配到不同的可用激活码,而不会遇到数据库并发问题?是否有更好的模式来处理有多个激活码可用的情况?
任何有关处理此类并发问题的模式或方法的见解将不胜感激!
我会使用 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
});
}