异步编程求助:依赖任务执行时重复创建记录问题排查
你的问题核心在于重复ID的并发处理:当循环遇到两个相同的ID(比如列表里的两个3)时,第一个ID的CreateRecord还没完成(第三方供应商那边还没写入记录),第二个ID就已经执行了GetRecord,此时记录还不存在,所以会触发重复创建。另外,用Task.Run(async () => await GetRecord(id)).Result这种同步阻塞的方式不仅解决不了问题,还可能引发死锁或线程池资源耗尽的问题,完全不推荐。
下面给你几个逐步优化的解决方案:
1. 先对ID列表去重(最简单的场景)
如果你的ID列表是预先确定的,没有动态新增的情况,第一步可以先去掉重复的ID,这样同一个ID只会被处理一次:
// 先去重,确保每个ID只处理一次 var uniqueIds = IDs.Distinct().ToList(); foreach (var id in uniqueIds) { var record = await GetRecord(id); if (record == null) { await CreateRecord(id); } }
这个方案适合一次性处理固定列表的场景,能直接避免重复ID的问题。
2. 用异步锁控制同一ID的串行执行(动态场景)
如果你的ID列表是动态生成的,无法提前去重(比如来自实时数据流),那需要给每个ID加一把异步锁,确保同一ID的GetRecord和CreateRecord操作是串行执行的,避免并发冲突:
// 用字典存储每个ID对应的异步锁,保证同一ID的操作串行 private readonly Dictionary<int, SemaphoreSlim> _idLocks = new Dictionary<int, SemaphoreSlim>(); // 辅助方法:获取对应ID的锁(线程安全) private SemaphoreSlim GetLockForId(int id) { lock (_idLocks) { if (!_idLocks.TryGetValue(id, out var semaphore)) { semaphore = new SemaphoreSlim(1, 1); // 初始计数1,最大计数1,相当于独占锁 _idLocks.Add(id, semaphore); } return semaphore; } } // 处理逻辑 foreach (var id in IDs) { var semaphore = GetLockForId(id); await semaphore.WaitAsync(); // 等待锁释放 try { var record = await GetRecord(id); if (record == null) { await CreateRecord(id); } } finally { semaphore.Release(); // 必须释放锁,避免死锁 } }
这个方案的原理是:当第一个ID=3的请求进来时,会获取锁并执行操作;第二个ID=3的请求会等待锁释放,此时第一个请求的CreateRecord已经完成,第三方那边已经有记录了,所以第二个请求的GetRecord会返回非null,不会重复创建。
3. 额外优化:复用HttpClient
你的代码里每次调用GetRecord和CreateRecord都新建HttpClient,这是个不好的习惯——频繁创建HttpClient会耗尽系统的Socket资源。建议复用同一个HttpClient实例:
// 静态复用HttpClient实例,避免频繁创建 private static readonly HttpClient _httpClient = new HttpClient(); public async Task<Record> GetRecord(int id) { // 构建请求逻辑... // var response = await _httpClient.SendAsync(request); // 根据响应返回Record或null } public async Task<bool> CreateRecord(int id) { // 构建请求逻辑... // var response = await _httpClient.SendAsync(request); // 根据响应返回true或false }
如果是ASP.NET Core项目,更推荐用IHttpClientFactory来创建和管理HttpClient,它会自动处理实例的生命周期。
补充:分布式场景的扩展
如果你的应用是多实例部署的(比如多个服务器同时运行),单进程的锁就不够用了,这时候需要用分布式锁(比如基于Redis、ZooKeeper的锁)来保证不同实例之间同一ID的操作串行化。不过这个是进阶场景,如果你只是单进程应用,前面的方案已经足够解决问题。
内容的提问来源于stack exchange,提问作者blue piranha

