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

另外我们就是使用锁来避免并发问题,EF Core 主要提供了乐观并发和悲观并发两种策略来应对并发问题。下面是一个对比表格,帮助你快速了解这两种策略的核心区别:
| 特性 | 乐观并发控制 | 悲观并发控制 |
|---|---|---|
| 基本原理 | 假设冲突不常发生,更新时检查数据是否被修改过 | 假设冲突经常发生,操作时直接锁定资源 |
| 实现方式 | 使用并发令牌(如时间戳/RowVersion)或属性标记 | 使用数据库锁(如 UPDLOCK) |
| 性能影响 | 冲突较少时性能较好,冲突较多时重试或异常处理开销大 | 保证数据强一致性,但可能引发线程阻塞和性能下降 |
| 适用场景 | 读多写少,数据冲突概率较低的应用 | 写操作非常频繁,且数据竞争激烈的场景 |
| EF Core支持 | 原生支持 | 需通过原生SQL或事务手动实现 |
乐观并发的核心在于,它在执行更新操作时,会检查要更新的记录自从被读取后,是否已被其他操作修改过(通过并发令牌比对)。如果已被修改,则意味着发生了冲突,EF Core 会抛出 DbUpdateConcurrencyException 异常。
如何实现乐观并发
实现乐观并发,主要方式是使用并发令牌(Concurrency Token)。EF Core 提供了两种主要方式:
[ConcurrencyCheck] 特性:在模型类的属性上标注
[ConcurrencyCheck],该属性就会被用作并发检查的依据。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 应用中处理并发问题。