如何修复 ASP.NET Web API 2 中并发请求的更新问题

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

我正在使用 .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."
});

您有什么建议? (我的目标是处理多个并发请求,这就是我试图找到正确方法的原因。)

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

我不知道你的 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 类名称。

祝你好运。

© www.soinside.com 2019 - 2024. All rights reserved.