如何提升.NET REST API批量上传速度同时避免SQL唯一键冲突?
解决SQL Server批量插入的速度与冲突问题
为什么提前校验在Azure环境失效?
- 本地localhost是单实例运行,校验和插入的时间差内不会有其他请求修改数据,但Azure是多实例部署,不同实例的请求可能同时操作同一数据,校验和插入不是原子操作,会出现竞态条件:实例A校验记录不存在,实例B在A插入前先插入了该记录,最终A插入时触发唯一键冲突。
解决方案:无需二选一,兼顾速度与可靠性
下面是几种可行的方案,按推荐程度排序:
方案1:使用SQL Server表值参数(TVP) + 原子性插入语句
通过自定义SQL结合表值参数,实现一次批量插入且跳过冲突行,既保证速度,又避免整组失败。
步骤:
- 在SQL Server中创建对应的表值类型:
CREATE TYPE dbo.MyDataItemType AS TABLE ( Id INT PRIMARY KEY, Name NVARCHAR(100) NOT NULL, -- 其他字段与你的目标表结构匹配 );
- 在.NET代码中构造TVP并执行批量插入:
// 假设你的数据模型是MyDataItem var batchItems = current200Items; // 当前要插入的200条数据 // 构造表值参数 var tvpParam = new SqlParameter("@BatchItems", SqlDbType.Structured) { TypeName = "dbo.MyDataItemType", Value = batchItems.AsEnumerable() }; // 执行原子性插入,跳过已存在的记录 await _dbContext.Database.ExecuteSqlRawAsync(@" INSERT INTO YourTargetTable (Id, Name, OtherColumns) SELECT Id, Name, OtherColumns FROM @BatchItems WHERE NOT EXISTS ( SELECT 1 FROM YourTargetTable t WHERE t.Id = @BatchItems.Id -- 替换为你的唯一键字段 )", tvpParam);
- 优势:原生SQL支持,无需第三方库,插入操作是原子性的,彻底避免竞态问题,速度和批量
SaveChanges相当甚至更快。
方案2:使用EF Core批量操作库(如EFCore.BulkExtensions)
第三方库封装了批量操作细节,支持一键忽略冲突或更新冲突行,代码更简洁。
- 安装NuGet包:
Install-Package EFCore.BulkExtensions - 调用批量插入方法并设置冲突处理:
await _dbContext.BulkInsertAsync(current200Items, options => { options.IgnoreOnMergeConflict = true; // 忽略冲突行,正常插入其他行 options.BatchSize = 200; // 保持你测试过的最优批量大小 });
- 优势:代码简洁,底层用高效的Bulk Copy/TVP实现,速度快,同时内置冲突处理逻辑,无需自己写复杂SQL。
方案3:捕获异常后拆分重试(备选)
如果不想用自定义SQL或第三方库,可以捕获DbUpdateException,解析冲突行后单独处理,再插入剩余行。
try { await _dbContext.AddRange(current200Items); await _dbContext.SaveChangesAsync(); } catch (DbUpdateException ex) { var sqlException = ex.InnerException as SqlException; // SQL Server唯一键冲突错误码为2601或2627 if (sqlException != null && (sqlException.Number == 2601 || sqlException.Number == 2627)) { // 从错误信息中提取冲突的键值(需根据实际错误提示调整解析逻辑) var conflictId = ExtractConflictIdFromMsg(sqlException.Message); // 移除冲突行,重新插入剩余数据 var validItems = current200Items.Where(item => item.Id != conflictId).ToList(); if (validItems.Any()) { _dbContext.AddRange(validItems); await _dbContext.SaveChangesAsync(); } // 记录冲突行错误信息并返回给前端 // ... } else { // 处理其他数据库异常 throw; } }
- 注意:解析错误信息的逻辑需要根据你的唯一键字段调整,这个方法相对繁琐,适合特殊场景。
总结
通过原子性的批量插入操作(方案1或2),可以彻底解决竞态条件导致的冲突问题,同时保持批量插入的速度,不需要在速度和可靠性之间做取舍。
内容的提问来源于stack exchange,提问作者John Henckel
相关产品推荐
相关产品推荐

