You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ASP.NET Core Web API第三方服务调用提速方案咨询

批量调用第三方服务的优化方案

核心问题分析

当前实现为串行异步调用,每次请求完成后才发起下一次任务,对于20000条数据需执行2000次串行请求,总耗时约为单次请求时间×2000,用户等待周期过长。通过并行异步调用可大幅缩短总耗时,但需重点关注第三方服务并发限制、线程安全、数据库操作一致性这三个核心点。

可行优化方案

1. 使用Parallel.ForEachAsync实现并行调用

.NET 6+引入的Parallel.ForEachAsync专为异步并行批量任务设计,支持控制并发度,避免过度施压第三方服务。

关键注意事项:

  • 并发度控制:必须通过MaxDegreeOfParallelism设置合理并发数(建议先设5-10,根据第三方服务限流规则调整),防止被封禁或返回限流错误。
  • 线程安全集合:原代码中List<Coupon>非线程安全,并行场景下需替换为ConcurrentBag<Coupon>,或用锁保护普通集合的读写操作。
  • 数据库操作优化:避免在并行任务中共享_unitOfWork(多数仓储实现非线程安全),建议先收集所有待插入数据,最后批量提交至数据库,减少IO开销。
  • 错误隔离:单个并行任务失败不能终止全部任务,需捕获异常并记录,最后汇总失败情况反馈给用户。

2. 额外性能优化点

  • 复用HttpClient:原ABCApiCaller每次调用创建HttpWebRequest,建议改用ASP.NET Core内置的IHttpClientFactory注入HttpClient,复用连接池提升请求效率。
  • 提前计算请求批次:预先算出总批次数(totalBatches = (requestedAmount + 9) / 10,向上取整),生成所有批次的请求参数后再并行处理。

修改后的代码示例

重构后的主方法

public async Task<HttpResponseMessage> CallServiceMultiple(RequestDto requestDto)
{
    var customerId = int.Parse(_http.HttpContext.User.Claims.FirstOrDefault(x => x.Type == "UserId")?.Value);
    var coupons = new ConcurrentBag<Coupon>(); // 线程安全集合存储优惠券
    var allGameRequests = new List<GameRequest>();
    var allGameResponses = new List<GameResponse>();
    var allGameConfirmRequests = new List<GameConfirmRequest>();
    var allGameConfirmResponses = new List<GameConfirmResponse>();
    var allConfirmCancels = new List<ConfirmCancel>();

    var requestedAmount = requestDto.quantity;
    var requestClientCode = requestDto.productCode;

    // 产品代码转换
    var productCode = _unitOfWork.ProductCodeRepository.GetByCode(p => p.clientCode == requestDto.productCode);
    if (productCode != null)
    {
        requestDto.productCode = productCode.gameCode;
    }

    // 计算总批次数并生成批次请求量列表
    var totalBatches = (requestedAmount + 9) / 10;
    var batchQuantities = Enumerable.Range(0, totalBatches).Select(_ =>
    {
        var count = Math.Min(requestedAmount, 10);
        requestedAmount -= count;
        return count;
    }).ToList();

    // 配置并行选项:限制并发度
    var parallelOptions = new ParallelOptions
    {
        MaxDegreeOfParallelism = 8 // 根据第三方服务实际限制调整
    };

    // 并行处理所有批次
    await Parallel.ForEachAsync(batchQuantities, parallelOptions, async (batchCount, cancellationToken) =>
    {
        // 每个批次独立构建请求对象
        var gameRequest = _mapper.Map<RequestOyunPalasDto, GameRequest>(requestDto);
        gameRequest.quantity = batchCount;
        gameRequest.referenceId = Guid.NewGuid();
        gameRequest.service = "ABC";
        gameRequest.customerID = customerId;

        var gameRequestDto = _mapper.Map<GameRequest, GameRequestDto>(gameRequest);
        gameRequest = Utilities.Utilities.CreateSignature(gameRequestDto, RequestType.Initiate, _configuration);

        // 调用初始化接口
        var response = await ABCApiCaller.CallRazer(gameRequest, "purchaseinitiation", _configuration);
        var htmlResponse = await response.Content.ReadAsStringAsync();
        var gameResponse = JsonConvert.DeserializeObject<GameResponse>(htmlResponse);

        // 锁保护非线程安全集合的写入
        lock (allGameRequests)
        {
            allGameRequests.Add(gameRequest);
            allGameResponses.Add(gameResponse);
        }

        if (gameResponse.initiationResultCode == "00")
        {
            gameRequestDto = _mapper.Map<GameRequest, GameRequestDto>(gameRequest);
            gameRequestDto.validatedToken = gameResponse.validatedToken;
            var gameConfirmRequest = Utilities.Utilities.CreateSignature(gameRequestDto, RequestType.Confirm, _configuration);
            var gameConfirmRequests = _mapper.Map<GameRequest, GameConfirmRequest>(gameConfirmRequest);

            // 调用确认接口
            response = await ABCApiCaller.CallRazer(gameConfirmRequest, "purchaseconfirmation", _configuration);
            htmlResponse = await response.Content.ReadAsStringAsync();
            var settings = new JsonSerializerSettings { DateTimeZoneHandling = DateTimeZoneHandling.Local };
            var gameConfirmResponse = JsonConvert.DeserializeObject<GameConfirmResponse>(htmlResponse, settings);

            // 设置响应元数据
            gameConfirmResponse.service = "ABC";
            gameConfirmResponse.productCode = requestClientCode;
            gameConfirmResponse.status = 1;

            // 收集优惠券
            foreach (var coupon in gameConfirmResponse.coupons)
            {
                coupons.Add(coupon);
            }

            // 锁保护数据库数据集合写入
            lock (allGameConfirmRequests)
            {
                allGameConfirmRequests.Add(gameConfirmRequests);
                allGameConfirmResponses.Add(gameConfirmResponse);
                allConfirmCancels.Add(new ConfirmCancel { referenceId = gameConfirmResponse.referenceId, status = 1 });
            }
        }
    });

    // 批量插入数据库
    _unitOfWork.GameRepository.InsertRange(allGameRequests);
    _unitOfWork.GameResponseRepository.InsertRange(allGameResponses);
    _unitOfWork.GameConfirmRequestRepository.InsertRange(allGameConfirmRequests);
    _unitOfWork.GameConfirmResponseRepository.InsertRange(allGameConfirmResponses);
    _unitOfWork.ConfirmCancelRepository.InsertRange(allConfirmCancels);

    await _unitOfWork.SaveAsync();

    // 构建最终响应
    var validConfirmResponse = allGameConfirmResponses.FirstOrDefault();
    if (validConfirmResponse == null)
    {
        return new HttpResponseMessage(HttpStatusCode.BadRequest)
        {
            Content = new StringContent("未获取到有效优惠券数据", System.Text.Encoding.UTF8, "application/json")
        };
    }

    validConfirmResponse.coupons = coupons.ToList();
    validConfirmResponse.quantity = requestDto.quantity;
    validConfirmResponse.totalPrice = validConfirmResponse.unitPrice * requestDto.quantity;

    var resultResponse = JsonConvert.SerializeObject(validConfirmResponse, Formatting.Indented,
        new JsonSerializerSettings()
        {
            ReferenceLoopHandling = ReferenceLoopHandling.Ignore
        });

    return new HttpResponseMessage
    {
        StatusCode = System.Net.HttpStatusCode.OK,
        Content = new StringContent(resultResponse, System.Text.Encoding.UTF8, "application/json"),
    };
}

优化ABCApiCaller使用HttpClient

// 建议将ABCApiCaller改为非静态类,注入IHttpClientFactory和IConfiguration
public class ABCApiCaller
{
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly IConfiguration _configuration;

    public ABCApiCaller(IHttpClientFactory httpClientFactory, IConfiguration configuration)
    {
        _httpClientFactory = httpClientFactory;
        _configuration = configuration;
    }

    public async Task<HttpResponseMessage> CallRazer(GameRequest gameRequest, string url)
    {
        var httpClient = _httpClientFactory.CreateClient();
        var baseUrl = _configuration.GetSection("ABC:Url").Value;
        var fullUrl = $"{baseUrl}{url}";

        HttpContent content;
        if (url == "Product/")
        {
            var config = new MapperConfiguration(cfg => { cfg.CreateMap<GameRequest, ProductRequestDto>(); });
            var iMapper = config.CreateMapper();
            var productRequest = iMapper.Map<GameRequest, ProductRequestDto>(gameRequest);
            var keyValueContent = productRequest.ToKeyValues();
            content = new FormUrlEncodedContent(keyValueContent);
        }
        else
        {
            var keyValueContent = gameRequest.ToKeyValues();
            content = new FormUrlEncodedContent(keyValueContent);
        }

        content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/x-www-form-urlencoded");
        return await httpClient.PostAsync(fullUrl, content);
    }
}

额外注意事项

  • 第三方服务限流:并行调用前务必确认服务的并发请求上限,超过限制可能触发临时封禁或错误响应,建议先小批量测试验证。
  • 事务一致性:若要求所有数据库操作原子性(全部成功或全部回滚),需将批量插入逻辑包裹在事务中(依赖_unitOfWork的事务支持)。
  • 超时设置:为HttpClient配置合理的超时时间,避免单个请求超时导致整体任务延迟。
  • 日志监控:在并行任务中添加详细日志,便于排查单个批次的失败原因。

内容的提问来源于stack exchange,提问作者raysefo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 07:25:14