ef core 默认不检测并发冲突,需显式配置并发令牌(如[timestamp]特性配合byte[] rowversion字段且数据库为rowversion类型)才会抛dbupdateconcurrencyexception。

EF Core 不会自动检测并发冲突,SaveChangesAsync 静默覆盖别人改过的数据是默认行为——除非你显式配置了并发令牌,否则根本不会抛 DbUpdateConcurrencyException。
为什么加了 [Timestamp] 还是没报异常
常见错误是只加了特性,但没满足底层依赖条件:
-
RowVersion字段类型必须是byte[],且不能为null(推荐初始化为Array.Empty<byte>()</byte>) - SQL Server 中该列必须是
rowversion类型(迁移脚本里会自动生成,但手动建表时容易漏) - 如果同时用了 Fluent API 和特性,
IsRowVersion()会覆盖[Timestamp];反之,只用特性而数据库不支持rowversion(比如 PostgreSQL),也不会生效 - 字段名不叫
RowVersion也没关系,但必须在OnModelCreating里用IsRowVersion()显式声明,否则 EF Core 不识别
捕获 DbUpdateConcurrencyException 后重试失败的真正原因
很多人写了 try-catch,也调了 SaveChangesAsync 第二次,但依然失败——因为没刷新原始快照:
-
entry.OriginalValues是 EF Core 用来生成 WHERE 条件的值,它不会自动更新 - 必须手动调用
await entry.GetDatabaseValuesAsync()拿到当前库值,再用entry.OriginalValues.SetValues(databaseValues)覆盖原始快照 - 只设
RowVersion不够:如果实体还有其他业务字段(如Stock)也被别人改过,SetValues()会一并同步,避免“部分字段被覆盖却未感知” - 别在一个
DbContext实例里无限重试,建议限制 2–3 次,超限后交由上层决定是否提示用户或丢弃变更
[ConcurrencyCheck] 和 IsRowVersion() 到底选哪个
优先用 IsRowVersion()(即 byte[] RowVersion):
-
[ConcurrencyCheck]只校验被标记的字段,比如只标了UpdatedAt,那别人改了Price就不会触发冲突——这不是疏忽,是设计如此 -
RowVersion是整行级原子标识,只要该行被更新过就一定变,检测更彻底 -
[ConcurrencyCheck]的字段需你手动维护(比如每次SaveChanges前赋新值),漏一次就等于关掉锁 - SQL Server 的
rowversion、PostgreSQL 的xmin、SQLite 的sqlite_version是数据库原生支持,无需应用层干预
独立关联会让并发控制完全失效
如果你的实体用的是独立关联(即没声明外键属性,靠 EF 内部管理导航关系),即使加了 [Timestamp],DbUpdateConcurrencyException.Entries 也可能为空或无法准确定位冲突来源:
- 必须显式声明外键属性,例如
public int? AuthorId { get; set; } - 并在
OnModelCreating中用HasForeignKey明确配置,否则 EF Core 在生成 UPDATE SQL 时可能跳过并发条件 - 这个限制在 EF Core 8 中仍未修复,生产环境务必规避
最难的不是写对 IsRowVersion() 或捕获异常,而是业务层要提前定义清楚:什么场景下该“以数据库为准”,什么场景下该“以用户输入为准”。没规则的重试循环,只会把问题从数据库推给用户。











