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

多实例部署场景下EF Core并发访问重复拉取数据问题如何解决?

问题结论

首先明确:这段代码运行在多实例场景下一定会出现并发问题,多个实例会拉取到重复的记录。

问题原因

你的逻辑分为「查询未处理记录」和「标记为处理中」两个独立步骤,两个实例可以同时执行完第一步查询,拿到完全相同的100条记录,之后再分别执行标记更新,最终双方都会认为自己拿到了这批记录的处理权,就会出现重复处理的情况。
另外你给出的示例代码有语法错误:records是List集合类型,不能直接给集合赋值IsProcessing属性,需要遍历每个元素单独赋值,或者使用批量更新语法。


解决方案

下面是几种常用的防重复方案,可根据你的业务场景选择:

方案1:使用数据库原子操作(优先推荐)

将「查询+标记」两个步骤合并为一个原子SQL语句,靠数据库本身的事务原子性保证不会有重复拉取,性能最高,实现也最简单。
不同数据库的语法略有差异,以SQL Server为例,可以使用UPDATE + OUTPUT语法一步完成拉取和标记:

// 原子操作:查询并标记100条未处理记录,不会重复
var records = await db.Records
    .FromSqlRaw(@"
        UPDATE TOP(100) Records 
        SET IsProcessing = 1 
        OUTPUT INSERTED.* 
        WHERE IsProcessing = 0
    ")
    .ToListAsync();
// 后续直接处理records即可,不需要再单独执行SaveChanges更新IsProcessing字段

MySQL的等效语法可以用SELECT ... FOR UPDATE加事务,或者用UPDATE加ORDER BY和LIMIT之后再查询,只要保证整个逻辑在同一个事务内即可。

方案2:悲观锁方案

如果不想写原生SQL,可以在查询时加数据库排他锁,把查询和更新逻辑包在同一个显式事务中,保证查询时其他事务不能读取这批记录:

using var transaction = await db.Database.BeginTransactionAsync();
try
{
    // 查询时加更新锁,阻止其他事务同时读取这批记录
    var records = await db.Records
        .FromSqlRaw("SELECT * FROM Records WITH (UPDLOCK, HOLDLOCK) WHERE IsProcessing = 0")
        .Take(100)
        .ToListAsync();
    
    foreach (var record in records)
    {
        record.IsProcessing = true;
    }
    await db.SaveChangesAsync();
    await transaction.CommitAsync();
    
    // 后续处理records
}
catch
{
    await transaction.RollbackAsync();
    throw;
}

该方案实现简单,适合并发量不高的场景,缺点是锁粒度较大,高并发下可能出现较多锁等待。

方案3:分布式锁方案

如果你的业务不允许直接操作数据库锁,或者需要跨数据库、跨服务协调,可以引入分布式锁(基于Redis、ZooKeeper等中间件实现):

  • 执行拉取记录逻辑前,先尝试获取全局分布式锁
  • 只有抢到锁的实例才能执行查询和标记逻辑,执行完成后释放锁
  • 锁需要设置合理的超时时间,避免实例崩溃导致锁永久占用

该方案通用性强,不依赖数据库特性,缺点是需要额外引入中间件,需要处理锁超时、锁误释放等边界问题。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.25 06:15:05