EF Core 中 IsConcurrencyToken 触发版本冲突时实际抛出 DbUpdateConcurrencyException;该异常的 Entries 包含冲突实体,需用 GetDatabaseValuesAsync 获取最新值并仅更新 OriginalValues 中的并发令牌字段,避免 Reload 覆盖业务修改。

EF Core 中 IsConcurrencyToken 触发版本冲突时,实际抛出什么异常?
EF Core 在检测到并发冲突(比如 RowVersion 或 [Timestamp] 字段不匹配)时,不会抛出 DbUpdateConcurrencyException 以外的异常——这是唯一需要你显式捕获的类型。其他如 DbUpdateException 是更宽泛的更新失败兜底,不能用于精准识别“确实是版本冲突”。
常见误判场景:有人在 SaveChanges() 外层只 catch Exception,结果把网络超时、主键重复等错误和并发冲突混为一谈,导致本该重试的请求被当成致命错误处理。
- 必须用
try/catch (DbUpdateConcurrencyException ex)精准捕获 - 该异常的
Entries属性会列出所有发生冲突的EntityEntry,每个 entry 的OriginalValues和数据库当前值可对比查看差异 - 如果用了
SaveChangesAsync,对应 catchDbUpdateConcurrencyException即可,异步不会改变异常类型
怎么从 DbUpdateConcurrencyException 中安全提取并刷新冲突实体?
直接调用 entry.Reload() 是危险的:它会覆盖你内存中刚修改过的业务字段(比如用户改了姓名+地址,但版本号冲突,Reload 会把姓名也刷回旧值)。正确做法是只更新并发令牌字段,保留业务变更。
示例场景:订单实体有 Version(byte[])和 Status 字段,用户提交了新状态,但 DB 中 Version 已变:
catch (DbUpdateConcurrencyException ex)
{
foreach (var entry in ex.Entries)
{
// 只刷新并发令牌字段,不碰业务字段
var databaseValues = await entry.GetDatabaseValuesAsync();
if (databaseValues != null)
{
entry.OriginalValues["Version"] = databaseValues["Version"];
}
}
}-
GetDatabaseValuesAsync()是必须的,它真正查库拿到最新行值;Reload()会丢弃你的修改 - 务必检查
databaseValues != null,否则可能因记录已被删除而空引用 - 若实体含多个并发令牌(比如
Version+UpdatedAt),需逐个同步OriginalValues
IsConcurrencyToken 配置后,为什么更新仍不报并发异常?
最常见原因是没把并发令牌字段的原始值传进上下文——EF Core 比较的是 OriginalValues 和数据库当前值,如果你用 Attach() 或 Update(),EF 默认把所有字段设为 Modified,并发字段的 OriginalValues 会被设成当前内存值,等于自己比自己,永远通过。
- 正确做法:用
context.Entry(entity).State = EntityState.Unchanged,再手动设Property("Version").IsModified = true,并确保OriginalValues["Version"]是加载时的老值 - 更稳妥的方式是先
FindAsync()加载实体,再修改业务字段,这样OriginalValues自动带对 - 如果走原始 SQL 或存储过程更新,EF Core 完全不感知,
IsConcurrencyToken形同虚设 - 注意:SQL Server 的
rowversion类型在 EF Core 中必须映射为byte[],映射成long或int会导致比较失效
高并发下靠重试解决冲突,但重试逻辑写在哪最合适?
别在 Controller 或 Service 里裸写 while 循环重试——这会让业务逻辑和并发策略耦合,且难以测、难控制重试次数和间隔。
推荐方案:封装一个幂等性明确的仓储方法,内部处理重试边界:
public async Task<bool> TryUpdateOrderStatus(int orderId, string newStatus, byte[] expectedVersion)
{
for (int i = 0; i < 3; i++)
{
try
{
var order = await context.Orders
.AsNoTracking()
.FirstOrDefaultAsync(x => x.Id == orderId && x.Version == expectedVersion);
if (order == null) return false; // 版本已变或记录不存在
<pre class="brush:php;toolbar:false;"> var tracked = context.Orders.Attach(new Order { Id = orderId, Version = expectedVersion });
tracked.Property(x => x.Status).CurrentValue = newStatus;
tracked.Property(x => x.Version).IsModified = true;
await context.SaveChangesAsync();
return true;
}
catch (DbUpdateConcurrencyException)
{
if (i == 2) throw; // 最后一次失败才往上抛
await Task.Delay(50 * (i + 1)); // 指数退避
}
}
return false;}
关键点在于:每次重试都重新读取期望版本,而不是复用第一次的 expectedVersion —— 因为中间可能已有别人成功提交,当前最新版已不是你最初看到的那个。


















