奕玖科技 > 新闻中心 > 技术文章

asp.net core efcode 如何解决并发冲突问题

来源: 奕玖科技 Fly | 2025/9/2 9:26:08

在某些保存数据比较频繁的时候,加上又是使用异步的时候,加上编写SQL不严谨就很容易产生并发冲突的问题。首先在产生这种问题的时候我们第一时间检查自己的代码,有的时候我们只要理顺了代码把一些不合理的EfCode代码做出修改就可以解决很多的问题。另外在一些频率比较高的查询上建议New 一个DbContext,这样也会好很多。

20230111638090414141310678.jpg

另外我们就是使用锁来避免并发问题,EF Core 主要提供了乐观并发和悲观并发两种策略来应对并发问题。下面是一个对比表格,帮助你快速了解这两种策略的核心区别:

特性乐观并发控制悲观并发控制
基本原理假设冲突不常发生,更新时检查数据是否被修改过假设冲突经常发生,操作时直接锁定资源
实现方式使用并发令牌(如时间戳/RowVersion)或属性标记使用数据库锁(如 UPDLOCK
性能影响冲突较少时性能较好,冲突较多时重试或异常处理开销大保证数据强一致性,但可能引发线程阻塞和性能下降
适用场景读多写少,数据冲突概率较低的应用写操作非常频繁,且数据竞争激烈的场景
EF Core支持原生支持需通过原生SQL或事务手动实现

乐观并发的核心在于,它在执行更新操作时,会检查要更新的记录自从被读取后,是否已被其他操作修改过(通过并发令牌比对)。如果已被修改,则意味着发生了冲突,EF Core 会抛出 DbUpdateConcurrencyException 异常。

如何实现乐观并发

实现乐观并发,主要方式是使用并发令牌(Concurrency Token)。EF Core 提供了两种主要方式:

  1. [ConcurrencyCheck] 特性:在模型类的属性上标注 [ConcurrencyCheck],该属性就会被用作并发检查的依据。

  2. IsRowVersion 方法:配置一个字节数组 (byte[]) 属性作为 行版本(RowVersion) 或时间戳。这是推荐的方式,因为数据库会在每次更新行时自动生成新的行版本值。

下面是配置并发令牌的示例:

// 在模型类中定义 RowVersion 属性
public class House
{
    public int Id { get; set; }
    public string Owner { get; set; }
    [Timestamp] // 使用 Data Annotations 标记
    public byte[] RowVersion { get; set; }
}

// 或者在 DbContext 的 OnModelCreating 中配置(Fluent Api 方式)
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<House>(b =>
    {
        b.ToTable("T_Houses");
        b.Property(b => b.RowVersion).IsRowVersion(); // 设置为行版本
        // 或者对普通字段进行并发令牌标记
        // b.Property(p => p.Owner).IsConcurrencyToken();
    });
}

配置后,EF Core 在更新实体时会自动在 WHERE 子句中包含并发令牌的旧值。SQL 语句类似于:

UPDATE [T_Houses] SET [Owner] = @p0 WHERE [Id] = @p1 AND [RowVersion] = @p2;

如果此 UPDATE 语句影响的行数为 0(因为 RowVersion 已变),EF Core 就会抛出 DbUpdateConcurrencyException 异常。

处理并发冲突

发生并发冲突时,你需要捕获异常并决定如何处理

try
{
    Await _context.SaveChangesAsync();
    // 保存成功处理
}
catch (DbUpdateConcurrencyException ex)
{
    // 1. 获取引发异常的实体条目
    Var entry = ex.Entries.First();
    // 2. 获取当前数据库中的值(其他人已提交的值)
    var databaseValues = await entry.GetDatabaseValuesAsync();
    if (databaseValues == null)
    {
        // 记录已被删除
        Console.WriteLine("该记录已被删除。");
    }
    else
    {
        // 3. 获取数据库中的当前值
        string currentOwnerInDb = databaseValues.GetValue<string>(nameof(House.Owner));
        
        // 4. 可选择的方式:刷新实体,提示用户,或自定义合并逻辑
        // 方式A:用数据库的新值覆盖当前实体
        entry.OriginalValues.SetValues(databaseValues);
        // 或者方式B:重新加载实体
        // entry.Reload();

        Console.WriteLine($"并发冲突!记录已被 {currentOwnerInDb} 抢先修改。");
        // 这里可以根据业务逻辑决定是重试、报错还是合并数据
    }
}


悲观并发通过直接在数据库层面加锁(如 UPDLOCK)来防止其他操作修改数据,适用于数据竞争非常激烈的场景,但可能带来性能问题和死锁风险。EF Core 本身不直接支持悲观并发,通常需要通过执行原始 SQL 语句来实现

// 示例:在事务中使用 UPDLOCK 查询并更新:cite[1]
using (var transaction = _context.Database.BeginTransaction())
{
    // 使用原生 SQL 查询并加锁
    var house = _context.Houses
        .FromSqlRaw("SELECT * FROM T_Houses WITH (UPDLOCK) WHERE Id = {0}", id)
        .FirstOrDefault();

    if (house != null)
    {
        // ... 执行你的业务逻辑和更新 ...
        house.Owner = newOwner;
        _context.SaveChanges();
    }
    transaction.Commit();
}

其他优化建议

  • 及时处理 DbContext:DbContext 非线程安全。在多线程或异步环境中(如 ASP.NET Core Web API),务必确保每个操作使用独立的 DbContext 实例。通常通过依赖注入(AddDbContextPool)容器注册为 Scoped 生命周期即可,每个 HTTP 请求都会获得一个自己的实例。

  • 考虑使用异步操作:使用 SaveChangesAsync() 等方法可以提高高并发场景下的应用响应能力和吞吐量。

  • 评估批量操作:对于大量数据的更新或插入,考虑使用批量操作来减少数据库往返次数,提升性能。

总结

选择哪种并发控制策略,主要取决于你的具体业务场景和对数据一致性的要求程度。

  • 首选乐观并发:对于大多数 Web 应用,乐观并发通常是更合适的选择。它扩展性更好,避免了长期持有数据库锁,适合低冲突概率的场景。

  • 慎用悲观并发:仅在数据争用异常激烈、且乐观并发的重试/冲突处理逻辑无法满足严格的业务一致性要求时才考虑。使用时需注意其性能开销和死锁风险。

希望这些信息能帮助你更好地在 ASP.NET Core 应用中处理并发问题。


栏目导航
相关文章
文章标签
关于我们
公司简介
企业文化
资质荣誉
服务项目
高端网站定制
微信小程序开发
SEO排名推广
新闻动态
行业新闻
技术学院
常见问题
联系我们
联系我们
人才招聘
联系方式
Q Q:24722
微信:24722
电话:13207941926
地址:江西省抚州市赣东大道融旺国际3栋
Copyright©2008-2022 抚州市奕玖科技有限公司 备案号:赣ICP备2022010182号-1