我正在使用 .NET Framework 4.6.2 开发 ASP.NET Web API 2 项目。当我从 Postman 向特定端点发送两个并发请求时,数据库中仅更新一条记录。
这是我遇到问题的代码:
private HttpResponseMessage CallGameNew(RequestDto requestDto)
{
// Code omitted for brevity.
List<GameBank> gameBankResult = null;
//Query GameBank database
gameBankResult = _unitOfWork.GameBankRepository.GetGames(g =>
g.productCode == requestDto.productCode && g.referenceId == Guid.Empty);
if (gameBankResult != null && gameBankResult.Count() >= requestDto.quantity)
{
var k = requestDto.quantity - 1;
for (var i = k; i >= 0; --i)
{
gameBankResult[i].clientTrxRef = gameRequest.clientTrxRef;
gameBankResult[i].referenceId = gameRequest.referenceId;
gameBankResult[i].requestDateTime = DateTime.Now;
gameBankResult[i].responseDateTime = DateTime.Now;
}
//***** UPDATE GameBank *****
_unitOfWork.GameBankRepository.Update(gameBankResult[k]);
if (requestDto.quantity == 1)
{
//Code omitted for brevity.
}
}
_unitOfWork.Save();
return response;
}
我在上面的代码中尝试了 DbUpdateConcurrencyException 处理。这似乎只适用于 2 个并发请求,但如果有超过 2 个并发请求,我也会遇到同样的问题。
//Update GameBank
try
{
_unitOfWork.GameBankRepository.Update(gameBankResult[k]);
_unitOfWork.Save();
}
catch (DbUpdateConcurrencyException)
{
// Refresh and retry
gameBankResult[k] = _unitOfWork.GameBankRepository.GetByID(gameBankResult[k].GameBankID);
_unitOfWork.GameBankRepository.Update(gameBankResult[k]);
_unitOfWork.Save();
}
我尝试使用交易。但当我尝试在 Postman 中进行性能测试时,发送了 150 个请求(3 个虚拟用户使用固定负载配置文件持续 1 分钟)。表中仅更新了 134 条记录。我遇到了僵局。
private HttpResponseMessage CallGameNew(RequestDto requestDto)
{
// Code omitted for brevity.
List<GameBank> gameBankResult = null;
using (var scope = new TransactionScope(TransactionScopeOption.Required,
new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
{
//Query GameBank database
gameBankResult = _unitOfWork.GameBankRepository.GetGames(g =>
g.productCode == requestDto.productCode && g.referenceId == Guid.Empty);
if (gameBankResult != null && gameBankResult.Count() >= requestDto.quantity)
{
var k = requestDto.quantity - 1;
for (var i = k; i >= 0; --i)
{
gameBankResult[i].clientTrxRef = gameRequest.clientTrxRef;
gameBankResult[i].referenceId = gameRequest.referenceId;
gameBankResult[i].requestDateTime = DateTime.Now;
gameBankResult[i].responseDateTime = DateTime.Now;
}
//***** UPDATE GameBank *****
_unitOfWork.GameBankRepository.Update(gameBankResult[k]);
if (requestDto.quantity == 1)
{
//Code omitted for brevity.
}
}
_unitOfWork.Save();
scope.Complete();
}
return response;
}
因此我删除了事务并添加了 WebApiThrottle 来强制每秒发出请求。
服务是这样的:
private HttpResponseMessage CallGameNew(RequestDto requestDto)
{
HttpResponseMessage response = null;
//ProductCode Conversion
var productCode =
_unitOfWork.ProductCodeRepository.GetByCode(p => p.clientCode == requestDto.productCode);
if (productCode != null)
{
requestDto.productCode = productCode.gameCode;
}
var gameRequest = _mapper.Map<RequestDto, GameRequest>(requestDto);
//Unique reference ID
gameRequest.referenceId = Guid.NewGuid();
var gameRequestDto = _mapper.Map<GameRequest, GameRequestDto>(gameRequest);
//Create signature
gameRequest = UtilitiesWatson.CreateSignature(gameRequestDto, RequestType.Initiate);
//Set service
gameRequest.service = "OUR";
gameRequest.customerID = 5; //WATSON
gameRequest.clientTrxRef = requestDto.clientTrxRef; //WATSON
//Add initiation request into database
_unitOfWork.GameRepository.Insert(gameRequest);
_unitOfWork.Save();
GameBank gameBankResult = null;
gameBankResult = _unitOfWork.GameBankRepository.GetGame(g =>
g.productCode == requestDto.productCode && g.referenceId == Guid.Empty);
_unitOfWork.Save();
if (gameBankResult != null)
{
gameBankResult.clientTrxRef = gameRequest.clientTrxRef;
gameBankResult.referenceId = gameRequest.referenceId;
gameBankResult.requestDateTime = DateTime.Now;
gameBankResult.responseDateTime = DateTime.Now;
_unitOfWork.GameBankRepository.Update(gameBankResult);
_unitOfWork.Save();
var gameBankConfirmResponse =
_mapper.Map<GameBank, GameConfirmResponse>(gameBankResult);
gameBankConfirmResponse.purchaseStatusDate = DateTime.Now;
gameBankConfirmResponse.clientTrxRef = gameRequest.clientTrxRef;
//ProductCode Conversion
var productCodeReverse = _unitOfWork.ProductCodeRepository.GetByCode(p =>
p.gameCode == requestDto.productCode);
if (productCodeReverse != null)
{
gameBankConfirmResponse.productCode = productCodeReverse.clientCode;
}
var resultResponse = JsonConvert.SerializeObject(gameBankConfirmResponse,
Formatting.Indented,
new JsonSerializerSettings()
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
});
response = new HttpResponseMessage
{
StatusCode = System.Net.HttpStatusCode.OK,
Content = new StringContent(resultResponse, System.Text.Encoding.UTF8,
"application/json"),
};
//Set service
gameBankConfirmResponse.service = "OUR";
gameBankConfirmResponse.clientTrxRef = requestDto.clientTrxRef;
_unitOfWork.GameConfirmResponseRepository.Insert(gameBankConfirmResponse);
_unitOfWork.Save();
}
return response;
}
这是 WebApiConfig 中的 WebApiThrottle:
config.MessageHandlers.Add(new ThrottlingHandler()
{
Policy = new ThrottlePolicy(perSecond: 2, perMinute: 28)
{
IpThrottling = true,
EndpointThrottling = true,
EndpointRules = new Dictionary<string, RateLimits>
{
{ "api/v2/game/watson/purchase", new RateLimits { PerSecond = 1, PerMinute = 22, PerHour = 1100 } }
}
},
Repository = new CacheRepository(),
QuotaExceededMessage = "You may only perform this action every {0} seconds."
});
您有什么建议? (我的目标是处理多个并发请求,这就是我试图找到正确方法的原因。)
我不知道你的 Update 方法是如何工作的(希望它更新相关实体),并且我知道改变程序的整个结构以消除“存储库编码”并使用像“这样的 DbContext 实例是更困难的”工作单元”,自然地在“使用”块内进行操作。不过,如果您有耐心在表中添加这样的列(假设您使用的是 SQL Server),则可以实现乐观并发:
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
[ConcurrencyCheck]
public byte[] RowVersion { get; set; }
你可以尝试使用我在 10 分钟内编写的这段代码,所以我们只能希望它能解决你的问题:
private HttpResponseMessage CallGameNew(RequestDto requestDto)
{
// Code omitted for brevity.
List<GameBank> gameBankResult = null;
while (true)
{
try
{
//Query GameBank database
gameBankResult = _unitOfWork.GameBankRepository.GetGames(g => g.productCode == requestDto.productCode && g.referenceId == Guid.Empty);
if (gameBankResult != null && gameBankResult.Count() >= requestDto.quantity)
{
var k = requestDto.quantity;
for (var i = 0; i < k; i++)
{
gameBankResult[i].clientTrxRef = gameRequest.clientTrxRef;
gameBankResult[i].referenceId = gameRequest.referenceId;
gameBankResult[i].requestDateTime = DateTime.Now;
gameBankResult[i].responseDateTime = DateTime.Now;
_unitOfWork.GameBankRepository.Update(gameBankResult[i]);
}
if (requestDto.quantity == 1)
{
//Code omitted for brevity.
}
_unitOfWork.GameBankRepository.Save();
break; //exit from while loop
}
}
catch
{
_dbContext.ChangeTracker.Clear(); //IS REQUIRED, so the next select will read new RowVersion also
Thread.Sleep((new Random()).Next(0, 1000)); //if you want to add a random pause 0-1 second
}
}
return response;
}
换线要有耐心:
_dbContext.ChangeTracker.Clear();
根据您的 dbcontext 类名称。
祝你好运。