EF Core 的 SaveChangesAsync 默认不处理并发冲突,需用 IsConcurrencyToken 标记字段启用乐观并发检查;捕获 DbUpdateConcurrencyException 后须手动合并业务逻辑并重试,避免简单覆盖,并考虑限流、退避及读写分离等高并发优化策略。
调用 SaveChangesAsync 时,EF Core 只是把待提交的变更翻译成 SQL 执行,不会主动检测或重试并发写入。如果两个请求同时读取同一行、各自修改后都调用 SaveChangesAsync,后提交者会直接覆盖前者的修改——除非你显式启用并发控制。
EF Core 的并发冲突检测依赖数据库层面的“版本戳”(如 rowversion 或 timestamp 字段),不是靠应用层锁或时间戳比对。必须在实体中声明一个属性并标记为并发令牌:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
1769966551 // SQL Server 专用,生成 rowversion 列
public byte[] RowVersion { get; set; }
}
或者用 Fluent API(更通用):
modelBuilder.Entity() .Property(p => p.RowVersion) .IsConcurrencyToken();
SaveChangesAsync 即使遇到数据库行已被修改,也不会抛出 DbUpdateConcurrencyException
1769966551 仅适用于 SQL Server;PostgreSQL/MySQL 需用 int 或 datetime 类型 + IsConcurrencyToken() 配合手动更新逻辑rowversion 或 DEFAULT NEXTVAL),不能由应用赋值EF Core 不会自动重试或合并。抛出 DbUpdateConcurrencyException 表示:当前实体的并发令牌值与数据库中不一致,即该行已被其他事务修改过。
Entries 属性包含所有冲突的 EntityEntry,可用来获取原始值、数据库当前值和当前修改值SaveChangesAsync
entry.OriginalValues.SetValues(entry.GetDatabaseValues()) 就重试,这会丢失用户本次修改的语义(比如“库存减1”变成“设为当前库存值”)try
{
await context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
foreach (var entry in ex.
Entries)
{
var databaseValues = await entry.GetDatabaseValuesAsync();
if (databaseValues == null)
{
throw new InvalidOperationException("数据库中已不存在该记录");
}
// 比如只允许更新 Price,保留 Name 和 RowVersion 来自数据库
entry.OriginalValues["Price"] = databaseValues["Price"];
entry.OriginalValues["RowVersion"] = databaseValues["RowVersion"];
}
// 重试(注意:需确保业务逻辑幂等,否则可能重复扣款等)
await context.SaveChangesAsync();
}
大量请求同时撞上同一行(如秒杀商品库存),反复重试会导致数据库压力陡增、响应延迟飙升,甚至线程池耗尽。
UPDATE ... WHERE version = @old AND stock >= @needed)+ 返回影响行数判断成败,绕过 EF Core 的跟踪开销真正难的不是捕获异常,而是判断“这次冲突要不要让用户重试”“哪些字段允许被覆盖”“失败后该提示什么”,这些都得贴着业务规则来设计,而不是套个 RetryAttribute 就完事。