C#如何避免多层循环重构批量提交订单至外部API的代码
订单批量提交外部API重构方案
现有实现的问题
- 三层嵌套循环可读性差,分组逻辑和执行逻辑耦合在一起,后续调整规则成本高
- 对订单集合做了多次重复遍历:先筛配送时段、再筛门店,数据量大时存在不必要的性能损耗,且硬编码配送时段枚举,后续新增时段需要修改代码
- 同步串行调用接口,批次多时总耗时为所有接口调用耗时之和,没有利用IO等待的空闲时间
- 无并发控制、异常处理、重试等生产环境必要逻辑,稳定性不足
重构核心思路
1. 分组逻辑简化
直接用LINQ的复合键分组,一次遍历就把所有「同门店+同配送时段」的订单归为一组,完全替代两层循环筛选的逻辑,不会漏数据,性能更优。
2. 职责拆分
把单批次提交逻辑抽成独立方法,和分组、分批逻辑解耦,后续调整接口参数、加测试、改重试逻辑都只需要改单个方法。
3. 受控异步并发
用SemaphoreSlim控制接口调用的最大并发数,既通过异步IO大幅缩短总处理耗时,又避免并发过高触发外部API的限流规则。
4. 预留容错扩展点
在提交方法里预留异常捕获、日志、取消令牌的位置,方便后续接入重试、熔断、失败补偿逻辑。
重构后实现代码
using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using MoreLinq; class Program { // 可迁移到配置文件管理 private const int BatchSize = 5; private const int MaxConcurrentApiCalls = 5; // 根据外部API限流阈值调整 private static readonly SemaphoreSlim _apiConcurrencyLimiter = new SemaphoreSlim(MaxConcurrentApiCalls); static async Task Main(string[] args) { var allOrders = GetProductOrders(); // 单次遍历完成 配送时段+门店 维度分组 var orderGroups = allOrders .GroupBy(o => new { o.DeliveryWindow, o.StoreName }) .ToList(); var batchSubmitTasks = new List<Task>(); foreach (var group in orderGroups) { // 每组按规则拆分为最多5笔的批次 foreach (var orderBatch in group.Batch(BatchSize)) { var batchOrders = orderBatch.ToList(); batchSubmitTasks.Add(SubmitOrderBatchAsync( batchOrders, group.Key.DeliveryWindow, group.Key.StoreName)); } } // 等待所有批次提交完成 await Task.WhenAll(batchSubmitTasks); } /// <summary> /// 单批次订单提交到外部API /// </summary> private static async Task SubmitOrderBatchAsync( List<ProductOrder> orders, string deliveryWindow, string storeName, CancellationToken ct = default) { await _apiConcurrencyLimiter.WaitAsync(ct); try { // 此处替换为实际的HttpClient异步POST逻辑 // 例:await httpClient.PostAsJsonAsync(submitUrl, new { StoreName = storeName, DeliveryWindow = deliveryWindow, Orders = orders }, ct); Console.WriteLine($"提交批次:门店[{storeName}],时段[{deliveryWindow}],订单数[{orders.Count}],订单ID:{string.Join(",", orders.Select(o=>o.ID))}"); await Task.Delay(100, ct); // 模拟接口IO耗时 } catch (Exception ex) { // 记录错误日志,需包含门店、时段、订单ID、异常信息,方便后续补偿 Console.WriteLine($"批次提交失败:门店[{storeName}],时段[{deliveryWindow}],错误:{ex.Message}"); // 可在此接入Polly重试策略 } finally { _apiConcurrencyLimiter.Release(); } } public static List<ProductOrder> GetProductOrders() { return new List<ProductOrder>() { new ProductOrder(){ ID = 1, DeliveryWindow ="Morning", StoreName = "StoreA", customerDetails = "Cust1"}, new ProductOrder(){ ID = 2, DeliveryWindow ="Morning", StoreName = "StoreA",customerDetails = "Cust2"}, new ProductOrder(){ ID = 3, DeliveryWindow ="Morning", StoreName = "StoreA",customerDetails = "Cust3"}, new ProductOrder(){ ID = 4, DeliveryWindow ="AfterNoon", StoreName = "StoreA",customerDetails = "Cust4"}, new ProductOrder(){ ID = 5, DeliveryWindow ="AfterNoon", StoreName = "StoreA",customerDetails = "Cust5"}, new ProductOrder(){ ID = 6, DeliveryWindow ="Morning", StoreName = "StoreB",customerDetails = "Cust6"}, new ProductOrder(){ ID = 7, DeliveryWindow ="Morning", StoreName = "StoreB",customerDetails = "Cust7"}, new ProductOrder(){ ID = 8, DeliveryWindow ="AfterNoon", StoreName = "StoreB",customerDetails = "Cust8"}, new ProductOrder(){ ID = 9, DeliveryWindow ="AfterNoon", StoreName = "StoreB",customerDetails = "Cust9"}, new ProductOrder(){ ID = 10, DeliveryWindow ="AfterNoon", StoreName = "StoreC",customerDetails = "Cust10"}, }; } } public class ProductOrder { public int ID { set; get; } public string StoreName { set;get;} public string DeliveryWindow { set; get; } public string customerDetails { set; get; } public string ProductDetails { set; get; } }
进一步优化建议
- 把批次大小、最大并发数、接口地址、超时时间等参数抽到配置文件,不需要改代码就能适配外部API的规则调整
- 推荐给接口调用增加重试、熔断、超时策略:比如遇到网络错误、5xx响应时自动重试2-3次,连续失败时暂停提交一段时间,避免雪崩
- 每个批次提交时记录完整日志:包含门店、配送时段、批次内订单ID列表、请求响应内容、耗时,出问题可以快速定位
- 如果订单量达到十万级以上,可以采用流式处理,不需要一次性把所有订单加载到内存分组,进一步降低内存占用
- 字符串比较时建议传入
StringComparison.OrdinalIgnoreCase,避免因为配送时段、门店名的大小写差异导致分组错误 - 失败的批次建议持久化存储对应订单ID,后续走补偿流程重新提交,避免丢单
- 如果不想引入MoreLinq依赖,可以用以下扩展方法实现同等分批功能:
public static IEnumerable<List<T>> Batch<T>(this IEnumerable<T> source, int batchSize) { var currentBatch = new List<T>(batchSize); foreach (var item in source) { currentBatch.Add(item); if (currentBatch.Count == batchSize) { yield return currentBatch; currentBatch = new List<T>(batchSize); } } if (currentBatch.Count > 0) yield return currentBatch; }
内容的提问来源于stack exchange,提问作者JackFrost
相关产品推荐
相关产品推荐

