EF Core新增County条目时,如何选IsolationLevel避免GroupId重复?
问题分析
代码问题
- 重复插入逻辑:事务内已执行
AddAsync并准备提交,但方法末尾再次调用AddAsync,会导致同一实体被重复插入,属于冗余且错误的逻辑。 - 缺失持久化操作:事务内仅调用
AddAsync未执行SaveChangesAsync,实体变更并未写入数据库,导致事务提交无实际意义。 - 并发竞态漏洞:读取
Max(GroupId)时未加锁,即使开启事务,ReadCommitted隔离级别仅保证不读取未提交数据,但无法阻止其他事务在当前事务提交前读取旧的最大值——这是并发场景下GroupId重复的核心原因。
测试方式问题
你的测试方式完全准确,精准复现了高并发下的竞态场景:通过断点暂停instance1的事务提交,让instance2读取到事务前的旧最大值,最终导致两者计算出相同的GroupId。这不是测试的问题,而是代码逻辑本身无法应对并发。
解决方案
第一步:修复基础代码错误
去掉重复插入逻辑,补充SaveChangesAsync完成持久化:
public async Task<County> Add(County entity, CancellationToken cancellationToken) { await using (var transaction = await _dbContext.Database.BeginTransactionAsync(IsolationLevel.ReadCommitted, cancellationToken)) { var maxGroupId = await _dbContext.County.MaxAsync(c => c.GroupId, cancellationToken); entity.GroupId = (maxGroupId ?? 0) + 1; await _dbContext.County.AddAsync(entity, cancellationToken); await _dbContext.SaveChangesAsync(cancellationToken); // 补充持久化操作 await transaction.CommitAsync(cancellationToken); } return entity; }
第二步:解决并发GroupId重复问题
推荐两种可靠方案:
方案1:显式加锁强制序列化读取
修改最大值查询逻辑,通过SQL加排他锁,确保同一时间只有一个事务能读取最新最大值:
// 替换原MaxAsync查询 var maxGroupId = await _dbContext.Database.SqlQuery<int?>(@" SELECT MAX(GroupId) FROM County WITH (UPDLOCK, HOLDLOCK) ").FirstOrDefaultAsync(cancellationToken); entity.GroupId = (maxGroupId ?? 0) + 1;
UPDLOCK:对读取结果加更新锁,直到事务结束HOLDLOCK:延长锁持有时间至事务结束,阻止其他事务读取旧值
方案2:使用数据库序列(推荐)
依赖数据库原生序列生成唯一GroupId,从根源避免竞态问题(以SQL Server为例):
- 创建数据库序列:
CREATE SEQUENCE CountyGroupIdSequence START WITH 1 INCREMENT BY 1;
- 在EF Core中配置实体自动使用序列值:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<County>() .Property(c => c.GroupId) .HasDefaultValueSql("NEXT VALUE FOR CountyGroupIdSequence"); }
- 简化Add方法,无需手动计算GroupId:
public async Task<County> Add(County entity, CancellationToken cancellationToken) { await _dbContext.County.AddAsync(entity, cancellationToken); await _dbContext.SaveChangesAsync(cancellationToken); return entity; }
补充:隔离级别的误区
修改隔离级别无效的原因是:即使提升到RepeatableRead或Serializable,也无法解决聚合查询(Max)的竞态问题——隔离级别仅能锁定行数据,无法锁定聚合结果,必须通过显式锁或数据库原生序列解决。
内容的提问来源于stack exchange,提问作者mszigetihu
相关产品推荐
相关产品推荐

