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
相关产品推荐
相关产品推荐

