EF Core 的 SaveChangesAsync 默认不处理并发冲突,需用 IsConcurrencyToken 标记字段启用乐观并发检查;捕获 DbUpdateConcurrencyException 后须手动合并业务逻辑并重试,避免简单覆盖,并考虑限流、退避及读写分离等高并发优化策略。

EF Core 的 SaveChangesAsync 默认不处理并发冲突
调用 SaveChangesAsync 时,EF Core 只是把待提交的变更翻译成 SQL 执行,不会主动检测或重试并发写入。如果两个请求同时读取同一行、各自修改后都调用 SaveChangesAsync,后提交者会直接覆盖前者的修改——除非你显式启用并发控制。
用 IsConcurrencyToken 标记字段才能触发乐观并发检查
EF Core 的并发冲突检测依赖数据库层面的“版本戳”(如 rowversion 或 timestamp 字段),不是靠应用层锁或时间戳比对。必须在实体中声明一个属性并标记为并发令牌:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
[Timestamp] // SQL Server 专用,生成 rowversion 列
public byte[] RowVersion { get; set; }
}
或者用 Fluent API(更通用):
modelBuilder.Entity() .Property(p => p.RowVersion) .IsConcurrencyToken();
- 没有这个标记,
SaveChangesAsync即使遇到数据库行已被修改,也不会抛出DbUpdateConcurrencyException -
[Timestamp]仅适用于 SQL Server;PostgreSQL/MySQL 需用int或datetime类型 +IsConcurrencyToken()配合手动更新逻辑 - 字段值必须由数据库自动生成(如
rowversion或DEFAULT NEXTVAL),不能由应用赋值
捕获 DbUpdateConcurrencyException 后必须手动处理冲突
EF Core 不会自动重试或合并。抛出 DbUpdateConcurrencyException 表示:当前实体的并发令牌值与数据库中不一致,即该行已被其他事务修改过。
1、对ASP内核代码进行DLL封装,从而大大提高了用户的访问速度和安全性;2、采用后台生成HTML网页的格式,使程序访问速度得到进一步的提升;3、用户可发展下级会员并在下级购买商品时获得差额利润;4、全新模板选择功能;5、后台增加磁盘绑定功能;6、后台增加库存查询功能;7、后台增加财务统计功能;8、后台面值类型批量设定;9、后台财务曲线报表显示;10、完善订单功能;11、对所有传输的字符串进行安全
- 异常对象的
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();
}
高并发下单纯重试容易引发雪崩,要加限流和退避
大量请求同时撞上同一行(如秒杀商品库存),反复重试会导致数据库压力陡增、响应延迟飙升,甚至线程池耗尽。
- 避免无限制循环重试,应设置最大重试次数(如 3 次)和指数退避(如 10ms → 30ms → 100ms)
- 对热点数据(如库存、账户余额),考虑改用数据库原生命令(
UPDATE ... WHERE version = @old AND stock >= @needed)+ 返回影响行数判断成败,绕过 EF Core 的跟踪开销 - 更彻底的解法是分离读写:用 Redis 做库存预占,再异步落库,把 EF Core 的并发压力转移到缓存层
真正难的不是捕获异常,而是判断“这次冲突要不要让用户重试”“哪些字段允许被覆盖”“失败后该提示什么”,这些都得贴着业务规则来设计,而不是套个 RetryAttribute 就完事。









