You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用EF AsSplitQuery读取SQL Server数据时并发写入致数据丢失

问题分析与解决方向

核心问题定位

你的场景是:基于.NET 7 + EF Core 7 + SQL Server的应用,通过带20个Include的查询结合.UseSplitQuery()加载大量数据到内存缓存并定期刷新;近期出现仅在Item更新/插入时,读取操作返回的一对多关系多方数据丢失的问题,无异常抛出,本地和Azure SQL环境均存在,具体表现为:

  • 被更新的Item丢失Multimedia等关联行,严重时所有Item的Multimedia行丢失
  • 若读取ItemContact中途出问题,后续Subtypes、EnvironmentalCertificates等所有一对多关系数据全为空
  • 写入操作通过单个SaveChanges()执行,理论上处于单事务中

已尝试更换SQL Server、升降.NET/EF版本、添加额外事务、减少Include等方案,目前仅靠异常检测器临时处理。

可能的原因及对应解决方向

1. SplitQuery并发读取与事务隔离级别冲突

.UseSplitQuery()会将多Include的查询拆分为多个独立SQL语句执行,这些语句之间没有原子性保证。当写入事务(SaveChanges())使用默认的READ COMMITTED隔离级别时,可能出现:

  • 第一个Split查询读取了Item主表数据(此时写入事务未提交)
  • 写入事务提交后,后续Split查询读取关联表数据时,可能读到部分更新后的状态,或因锁等待跳过部分数据
  • 极端情况下,若关联表查询遇到写入事务的锁,EF Core可能静默跳过结果(默认未开启严格的查询超时检测)

解决方向:

  • 提升读取缓存的事务隔离级别至REPEATABLE READ或SERIALIZABLE,确保SplitQuery的多个语句读取同一快照:
    using var transaction = _dbContext.Database.BeginTransaction(System.Data.IsolationLevel.RepeatableRead);
    var items = await _dbContext.Items.Include(...).UseSplitQuery().ToListAsync();
    transaction.Commit();
    
  • 开启EF Core查询超时设置,避免锁等待导致的静默失败:
    _dbContext.Database.SetCommandTimeout(60); // 设置60秒超时,超时会抛出异常而非静默丢弃结果
    

2. EF Core SplitQuery结果合并逻辑缺陷

EF Core 7的SplitQuery在处理大量关联Include时,可能存在结果合并bug:当主表与关联表的查询排序不一致,或关联表数据被写入事务修改导致行数不匹配时,EF Core无法正确关联主从数据,进而导致关联数据丢失。

解决方向:

  • 强制所有SplitQuery使用相同排序规则,保证主表与关联表的行顺序一致:
    var items = await _dbContext.Items
        .Include(i => i.Multimedia)
        .Include(i => i.Subtypes)
        .OrderBy(i => i.Id) // 统一按主键排序,确保SplitQuery结果顺序匹配
        .UseSplitQuery()
        .ToListAsync();
    
  • 升级至EF Core 8及以上版本,官方在后续版本中修复了多个SplitQuery合并逻辑的并发场景bug。

3. 缓存刷新与写入操作的并发冲突

缓存刷新的读取操作与写入操作无互斥机制,导致:

  • 写入操作修改Item及其关联数据时,缓存刷新的SplitQuery刚好读到部分修改后的状态,无法正确加载关联数据
  • 若写入操作在SplitQuery执行过程中修改关联表的外键或数据,EF Core的跟踪机制无法正确识别关联关系

解决方向:

  • 给缓存刷新和写入操作加分布式锁(如使用SQL Server的sp_getapplock),确保同一时间仅一个操作处理Item数据:
    // 写入操作加排他锁
    using var lockHandle = await _dbContext.Database.ExecuteSqlRawAsync("EXEC sp_getapplock @Resource = 'ItemCacheRefresh', @LockMode = 'Exclusive', @LockTimeout = 5000");
    try
    {
        await _dbContext.SaveChangesAsync();
    }
    finally
    {
        await _dbContext.Database.ExecuteSqlRawAsync("EXEC sp_releaseapplock @Resource = 'ItemCacheRefresh'");
    }
    
    // 缓存刷新读取操作加共享锁
    using var readLockHandle = await _dbContext.Database.ExecuteSqlRawAsync("EXEC sp_getapplock @Resource = 'ItemCacheRefresh', @LockMode = 'Shared', @LockTimeout = 5000");
    try
    {
        var items = await _dbContext.Items.Include(...).UseSplitQuery().ToListAsync();
        // 更新缓存逻辑
    }
    finally
    {
        await _dbContext.Database.ExecuteSqlRawAsync("EXEC sp_releaseapplock @Resource = 'ItemCacheRefresh'");
    }
    
  • 调整缓存刷新时机,避开写入高峰,或在写入操作完成后主动触发缓存刷新,替代定期刷新。

4. 关联表索引缺失或锁竞争导致查询异常

当写入操作频繁修改关联表(如Multimedia)时,若关联表缺少外键索引(如ItemId的非聚集索引),会导致查询时锁等待时间变长,EF Core可能静默丢弃部分结果。

解决方向:

  • 检查所有一对多关联的多方表,确保外键字段(如Multimedia.ItemId)存在非聚集索引
  • 监控SQL Server锁等待情况,通过sys.dm_tran_locks查看是否有长期等待的锁,针对性优化索引或查询逻辑

临时修复优化(替代异常检测器)

  • 在缓存刷新后添加校验逻辑:检查每个Item的关键关联数据是否存在,若缺失则立即重试查询(最多2-3次)
  • 给读取操作添加详细日志,记录每次SplitQuery的执行时间、返回行数,当关联数据缺失时输出对应SQL语句和执行上下文,便于排查根因

内容的提问来源于stack exchange,提问作者Henric Rosvall

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 22:35:18