EF Core并发冲突必须处理,否则必然丢数据;需配置[Timestamp]或IsRowVersion()启用乐观锁,校验RowVersion字段防止丢失更新。

EF Core 的并发冲突不是“要不要处理”的问题,而是“不处理就必然丢数据”——DbUpdateConcurrencyException 不是异常信号,是系统在替你拦住一次 丢失更新。
为什么 SaveChangesAsync 会静默覆盖别人改的值?
因为没配并发令牌。EF Core 默认对所有字段都“信任写入”,生成的 SQL 是:
UPDATE Products SET Stock = @newStock WHERE Id = @id
没有 WHERE 条件校验原始状态,后提交者直接覆盖前提交者的变更。典型表现是:前端两次点击“减库存”,数据库只减了 1 次。
- 根本原因不是高并发本身,而是实体类没标记
[Timestamp]或没配置IsRowVersion() -
[ConcurrencyCheck]也能触发检测,但只校验被标记字段——其他字段被改了不会报错 - SQL Server 的
rowversion列、PostgreSQL 的xmin、SQLite 的sqlite_version是数据库原生支持的自动递增值,推荐优先用
怎么让 Update 带上 RowVersion 校验?
必须同时满足两个条件:实体有 byte[] 类型字段 + 显式声明为版本列。缺一不可。
- 加
[Timestamp]特性(仅限 SQL Server):public byte[] RowVersion { get; set; } = [];同时确保数据库该列为rowversion类型 - 或用 Fluent API 统一控制(跨数据库更稳):
modelBuilder.Entity<product>().Property(p => p.RowVersion).IsRowVersion();</product>
- 别把
RowVersion设为nullable,EF Core 会跳过它;也别手动赋值,数据库自增才有效
捕获 DbUpdateConcurrencyException 后不能只 log 就完事
抛出异常说明数据库已拒绝本次更新,此时 entry.OriginalValues 还是旧值,entry.CurrentValues 是你改的新值,entry.GetDatabaseValues() 才是当前真实值——三者必须对齐才能重试。
- 最简重试逻辑是 reload + 再次提交:
entry.Reload(); // 用数据库最新值覆盖当前实体
- 更稳妥的做法是合并:把用户真正想改的字段(比如
Stock)从CurrentValues提出来,再和GetDatabaseValues()合并后设回CurrentValues - 千万别在 catch 块里直接
context.Entry(x).State = EntityState.Modified——这会丢掉原始值快照,下次再冲突就无法判断哪部分被别人动过
高频更新场景下,乐观锁重试可能引发雪崩
比如秒杀扣库存,1000 个请求同时读到库存=1,999 个会在第二次 SaveChanges 时失败。靠纯重试扛不住。
- 业务层要前置校验:查库存 → 判断是否足够 → 再走 EF 更新,避免无谓的数据库 round-trip
- 终极方案是绕过 EF,用原生 SQL 做原子扣减:
UPDATE Products SET Stock = Stock - 1 WHERE Id = @id AND Stock >= 1
然后检查RowsAffected - 如果必须用 EF,考虑把
RowVersion和业务字段(如Stock)一起标为并发令牌,让 WHERE 条件更严格
RowVersion 字段本身不参与业务逻辑,但它一旦失效,你就失去了判断“谁先改、谁后改”的唯一依据——这个 byte 数组不是装饰,是并发安全的锚点。











