Cosmos DB全量插入但SQL Server仅插入部分记录的事务问题排查
系统最初将数据存储在Azure SQL Server中,因表设计限制无法满足促销活动分析需求,故同时将数据存入NoSQL数据库优化查询与分析。为保证一致性,在向Azure SQL Server和Azure Cosmos DB保存数据时使用事务,认为_dbContext.SaveChangesAsync与transaction.CommitAsync均为原子操作,预期所有记录要么全量存储要么全部不存储。根据官方文档描述:"若任一命令失败,事务在释放时会自动回滚",但实际出现异常现象:
- Azure Cosmos DB成功插入所有数据
- Azure SQL Server仅插入部分记录
补充信息
预期行为
- Azure SQL Server与Cosmos DB应存储相同数量的记录
已检查内容
- 插入前
redPoints包含预期数量的元素 - SQL Server表无唯一约束、触发器或其他会导致静默拒绝的限制
- 近期更换日志系统,丢失历史日志,增加调试难度
- 未启用SQL重试策略
使用工具
- .NET: 6.0
- Microsoft.EntityFrameworkCore: 7.0.7
- MongoDB.Driver: 2.22.0
核心问题
- 为何Cosmos DB全量插入成功,而SQL Server仅插入部分记录?
SaveChangesAsync或事务处理是否存在潜在问题?- 如何进一步调试该问题?
简化代码
private async Task SaveRedPointsToDb( List<RedPoint> redPoints, CancellationToken cancellationToken ) { await using var transaction = await _dbContext.Database.BeginTransactionAsync( cancellationToken ); try { // Azure SQL Server await _dbContext.RedPoints.AddRangeAsync(redPoints, cancellationToken); await _dbContext.SaveChangesAsync(cancellationToken); // Azure Cosmos DB var redPointDocuments = redPoints.Select(x => new RedPointDocument(x)).ToList(); await _redPointCollection.InsertManyAsync(redPointDocuments); await transaction.CommitAsync(cancellationToken); } catch (Exception e) { // Log... } }
1. Cosmos全量成功但SQL仅部分插入的原因
你的代码逻辑中SQL的SaveChangesAsync执行在Cosmos插入之前,而实际结果是Cosmos全量成功、SQL部分成功,说明SaveChangesAsync未抛出异常,而是静默完成了部分插入。可能的具体原因:
- EF Core批量插入逻辑异常:EF Core 7.0.7的
AddRangeAsync+SaveChangesAsync默认会生成批量插入SQL,但如果实体映射存在问题(比如部分实体字段值不符合隐式数据类型要求但未触发异常)、或框架本身存在批量插入bug,可能导致部分记录被跳过。 - SQL Server端的静默处理:即使表无显式约束,若数据库开启了
SET ANSI_WARNINGS OFF等设置,可能导致字段截断、数据类型不匹配等问题被静默忽略,仅插入符合要求的记录。 - ChangeTracker状态异常:
AddRangeAsync未将所有redPoints标记为Added状态,导致SaveChangesAsync仅处理了部分实体。
2. SaveChangesAsync或事务处理的潜在问题
你的事务逻辑存在核心缺陷:
- 事务范围不覆盖Cosmos DB:EF Core的
BeginTransactionAsync仅针对当前DbContext关联的SQL Server生效,Cosmos DB的InsertManyAsync是独立操作,不受该事务控制。这种情况下,SQL操作和Cosmos操作完全是两个独立的事务,根本无法保证一致性。 - SaveChangesAsync的原子性误解:正常情况下,
SaveChangesAsync会将所有待插入记录打包成单一批量SQL提交给SQL Server,理论上是原子操作(要么全成功要么全失败)。但如果SQL Server端存在特殊配置(如IGNORE_DUP_KEY)或EF Core生成的SQL存在拆分逻辑,可能打破原子性。 - 异常处理不严谨:catch块仅做日志记录,未明确回滚事务(虽然
await using会自动释放事务并回滚,但如果异常被内层逻辑吞掉,可能导致事务意外提交)。
3. 进一步调试的方法
- 开启EF Core详细日志:在DbContext配置中添加日志过滤,记录生成的SQL语句和执行结果:
builder.Logging.AddFilter("Microsoft.EntityFrameworkCore.Database.Command", LogLevel.Information);
通过日志对比批量插入SQL的受影响行数与redPoints的数量,确认问题出在EF Core还是SQL Server端。
2. 直接验证SQL语句:将EF Core日志中的批量插入SQL复制到SSMS执行,观察是否能全量插入,以此隔离是框架问题还是数据库问题。
3. 检查ChangeTracker状态:在AddRangeAsync后、SaveChangesAsync前,打印_dbContext.ChangeTracker.Entries().Count(),确认所有redPoints都被标记为Added;在SaveChangesAsync后,检查所有实体的State是否变为Unchanged。
4. 查看SQL Server错误日志:即使应用日志丢失,SQL Server自身的错误日志可能记录插入时的警告(如字段截断、数据类型不匹配),可在SQL Server Management Studio中查看。
5. 缩小测试范围:先插入小批量数据(如10条),验证是否能全量成功,逐步增加数据量找到触发部分插入的临界点,缩小排查范围。
6. 添加中间校验逻辑:在SaveChangesAsync后立即查询SQL Server中RedPoints的数量,记录到日志,确认此时是否已经是部分插入状态,再执行Cosmos插入。
内容的提问来源于stack exchange,提问作者Ethan Guo

