Entity Framework布尔类型ConcurrencyToken并发控制失效排查
问题根因
- 核心逻辑冲突:你明确要求「每个IdChain仅可被使用一次」,但事务末尾又将
InUse改回false提交,等于用完的IdChain又变回可用状态,直接违背了业务规则,也给并发请求的修改留了可乘之机。 - 布尔类型并发令牌的局限性:EF的并发令牌校验逻辑是生成UPDATE语句时,在WHERE条件中携带属性查询时的原始值。当你在同一个事务内先把
InUse设为true、最后又改回false提交后,数据库中InUse的最终值和其他并发请求查询到的原始值都是false,后续请求的UPDATE语句的WHERE条件会匹配成功,自然不会触发DbUpdateConcurrencyException。 - 快照读的时间差问题:默认隔离级别下,多个并发请求会同时查询到
InUse = false的快照,后续修改会排队等待行锁,前序事务提交后如果InUse回到false,排队的修改就会全部执行成功。
解决方案
1. 修正业务逻辑(优先级最高)
既然要求每个IdChain仅可使用一次,事务末尾不要将用过的InUse改回false,永久标记为已使用即可,后续业务直接使用你新生成的MovementsChain实例:
// 直接删掉这行还原InUse状态的代码 // movementChain.InUse = false;
修改后只有第一个请求能成功将InUse设为true提交,后续请求要么查询时就发现InUse = true直接返回错误,要么UPDATE时WHERE条件匹配不到行,触发并发异常。
2. 替换并发令牌为版本号字段
布尔类型的并发令牌状态只有两种,很容易因为业务逻辑的来回修改导致并发校验失效,建议新增版本号字段做全局并发控制:
修改实体:
public partial class MovementsChain { public Guid IdChain { get; set; } public int IdRegistry { get; set; } public DateTime Creation { get; set; } public bool InUse { get; set; } // 新增版本号字段,EF会自动在更新时校验+自增 [Timestamp] public byte[] RowVersion { get; set; } }
[Timestamp]标记的字段会自动作为全局并发令牌,只要行被修改过版本号就会变更,后续请求的更新必然会触发并发异常,不需要再单独把InUse设为并发令牌。
3. 查询时加悲观锁避免快照并发
如果不想改现有字段结构,可以在查询MovementsChain时就加行级更新锁,让第一个拿到锁的请求直接阻塞其他所有请求的查询,前序事务提交后,后续请求查询到的InUse已经是true,直接返回错误即可:
var movementChain = await Context.MovementsChain .OrderByDescending(e => e.Creation) // EF Core 7+ 支持直接用WithLock加锁提示,低版本可以用FromSqlRaw加WITH (UPDLOCK, HOLDLOCK) .WithLock(LockMode.Update, LockBehavior.Hold) .FirstOrDefaultAsync(e => e.IdRegister == register.IdRegister);
4. 调整校验顺序
把客户端传入IdChain的校验提前到查询逻辑里,避免查到不匹配的IdChain,减少无效锁占用:
// 先校验客户端传入的IdChain合法性 if (request.IdChain == Guid.Empty) return new TransactionResult(Error.IdChainNotSpecified); var movementChain = await Context.MovementsChain .WithLock(LockMode.Update, LockBehavior.Hold) .OrderByDescending(e => e.Creation) .FirstOrDefaultAsync(e => e.IdRegister == register.IdRegister && e.IdChain == request.IdChain);
内容的提问来源于stack exchange,提问作者Cheshire Cat
相关产品推荐
相关产品推荐

