ef core 默认不检测并发冲突,savechanges() 静默覆盖他人修改;必须显式配置[timestamp](byte[]+rowversion)或[concurrencycheck]并发令牌,否则乐观锁无效。

EF Core 默认不检测并发冲突,SaveChanges() 会静默覆盖别人刚改过的数据——这不是 bug,是设计如此;必须显式配置并发令牌,否则“乐观锁”根本没生效。
为什么加了 [Timestamp] 还是没抛 DbUpdateConcurrencyException
常见错误不是代码写错了,而是漏掉了底层依赖条件:
-
[Timestamp]只对byte[]类型字段有效,且不能为null(推荐初始化为Array.Empty<byte>()</byte>) - SQL Server 中对应列必须是
rowversion类型(EF Core 迁移会自动生成,但手动建表或用旧脚本时极易漏) - PostgreSQL 需配合
bytea列 + 数据库层触发更新(如DEFAULT gen_random_bytes(8)),光打特性没用 - 如果同时用了
[Timestamp]和 Fluent API 的IsRowVersion(),后者会覆盖前者;反之,只用特性但数据库不支持行版本语义(比如 MySQL),照样不生效
用 [ConcurrencyCheck] 真的能替代 RowVersion 吗
不能。它看起来灵活,实际是“伪并发控制”:
-
[ConcurrencyCheck]只校验你标记的字段,比如只标了UpdatedAt,那别人改了Price就不会触发冲突 - 该字段必须由你每次更新前手动维护(如
entity.Version++),漏一次等于关掉锁 - 多个
[ConcurrencyCheck]字段会全部加入 WHERE 条件,提升精度也提高误冲突概率,但依然无法覆盖整行变更 -
RowVersion是数据库原生行级原子标识(SQL Serverrowversion、PostgreSQLxmin、SQLitesqlite_version),只要行被更新过就一定变,检测更彻底
捕获 DbUpdateConcurrencyException 后重试失败的真正原因
不是异常没捕获,而是没刷新 EF Core 内部用于生成 WHERE 条件的原始快照:
-
entry.OriginalValues是 EF Core 用来拼 WHERE 的值,它不会自动更新;不调await entry.GetDatabaseValuesAsync(),下次还是拿旧值去比 - 正确做法是:
entry.OriginalValues.SetValues(databaseValues)—— 这会一并同步所有字段(包括业务字段),避免“部分字段被覆盖却未感知” - 别在同一个
DbContext实例里无限重试,建议限制 2–3 次,超限后交由上层决定是否提示用户或丢弃变更 - 绝对不要在 catch 块里直接设
context.Entry(x).State = EntityState.Modified,这会丢掉原始值快照,后续再冲突就无法定位差异
独立关联会让并发控制完全失效
如果你的实体用的是独立关联(即没声明外键属性,靠 EF 内部管理导航关系),即使加了 [Timestamp],DbUpdateConcurrencyException.Entries 也可能为空或无法准确定位冲突来源:
- 必须把外键显式声明为属性,例如
public int? AuthorId { get; set; } - 并在
OnModelCreating中用HasForeignKey显式配置 - 这种限制在 EF Core 8 中仍未修复,生产环境务必规避
真正难的不是写对 [Timestamp] 或捕获异常,而是业务层要提前定义清楚:什么情况下该“让数据库赢”,什么场景下必须“让人赢”。没有规则的重试循环,只会让用户反复提交、反复被刷掉。











