EF Core中使用事务的条件插入未按预期生效(新手求助)
问题描述
我有以下两个数据库表/实体:
public class User { [Key] public Guid UserId { get; set; } public ICollection<Game> Games { get; set; } } public class Game { public Guid OwnerId { get; set; } public User Owner { get; set; } [Key] public int GameId { get; set; } public string GameName { get; set; } public ICollection<Question> Questions { get; set; } }
我是Entity Framework Core和C#的新手,想要实现一个功能:在Game表中插入数据,但限制每个User关联的Game数量不超过最大值。
我尝试了如下函数,使用事务后仍未达到预期效果。测试场景:某用户的Game表已有10条记录,maxGames设为11,调用CreateGame函数后,在延迟开始时手动插入一条Game记录,延迟结束后函数仍插入了第12条记录,我预期会触发回滚,保持Game记录数为11,但实际并未如此。
public async Task<Boolean> CreateGame(Guid ownerId, string gameName, int maxGames) { using (var transaction = _dbContext.Database.BeginTransaction()) try { Game game = new Game(ownerId: ownerId, gameName: gameName); // Count games for the given user var count = _dbContext.Games .Where(g => g.OwnerId == ownerId) .Select(g => g.OwnerId) .Count(); // Conditional insert if (count < maxGames) { await Task.Delay(10000); // Delay to simulate race condition await _dbContext.Games.AddAsync(game); await _dbContext.SaveChangesAsync(); await transaction.CommitAsync(); Console.WriteLine("Saved"); return true; } else { Console.WriteLine("Did not save"); return false; } } catch (Exception ex) { Console.WriteLine("Exception occurred"); Console.WriteLine(ex); await transaction.RollbackAsync(); return false; } }
请问为何代码未按预期工作?是不是因为判断条件不属于数据库逻辑,导致事务无法生效?
问题原因及解决方案
核心原因
你的代码失效主要有两个关键点:
- 事务隔离级别限制:EF Core默认用数据库的默认隔离级别(比如SQL Server是
READ COMMITTED),这个级别下,事务内查询到的count是事务启动时的快照数据,延迟期间其他事务插入的新记录不会被当前事务感知,所以判断条件依然成立,最终还是会插入数据。 - 非原子性操作:计数查询和插入是两个独立步骤,中间的延迟给了其他操作插入数据的窗口,即使加了事务,这两步也不是原子执行的,无法保证并发场景下的限制生效。
可行解决方案
要解决问题,必须把计数判断和插入操作变成数据库层面的原子操作,以下是两种常用方案:
方案1:原生SQL原子操作(推荐)
直接在数据库执行包含判断逻辑的SQL批处理,确保计数和插入是原子性的,用EF Core的ExecuteSqlRawAsync实现:
public async Task<bool> CreateGame(Guid ownerId, string gameName, int maxGames) { var sql = @" DECLARE @Count INT; SELECT @Count = COUNT(*) FROM Games WHERE OwnerId = @OwnerId; IF @Count < @MaxGames BEGIN INSERT INTO Games (OwnerId, GameName) VALUES (@OwnerId, @GameName); SELECT 1; END ELSE BEGIN SELECT 0; END"; var result = await _dbContext.Database.ExecuteSqlRawAsync( sql, new SqlParameter("@OwnerId", ownerId), new SqlParameter("@MaxGames", maxGames), new SqlParameter("@GameName", gameName)); return result == 1; }
这种方式把所有逻辑放在数据库端执行,完全避免了并发漏洞,性能和可靠性都最优。
方案2:高隔离级别+二次校验
如果不想写原生SQL,可以提升事务隔离级别到SERIALIZABLE,并在插入前重新校验计数:
public async Task<bool> CreateGame(Guid ownerId, string gameName, int maxGames) { using (var transaction = await _dbContext.Database.BeginTransactionAsync(System.Data.IsolationLevel.Serializable)) { try { // 第一次计数校验 var count = await _dbContext.Games .Where(g => g.OwnerId == ownerId) .CountAsync(); if (count >= maxGames) { Console.WriteLine("Did not save"); await transaction.RollbackAsync(); return false; } await Task.Delay(10000); // 插入前再次校验计数 count = await _dbContext.Games .Where(g => g.OwnerId == ownerId) .CountAsync(); if (count >= maxGames) { Console.WriteLine("Did not save after recheck"); await transaction.RollbackAsync(); return false; } var game = new Game { OwnerId = ownerId, GameName = gameName }; _dbContext.Games.Add(game); await _dbContext.SaveChangesAsync(); await transaction.CommitAsync(); Console.WriteLine("Saved"); return true; } catch (Exception ex) { Console.WriteLine("Exception occurred"); Console.WriteLine(ex); await transaction.RollbackAsync(); return false; } } }
SERIALIZABLE是最高隔离级别,会阻止其他事务在当前事务的查询范围内插入数据,但可能带来性能下降和死锁风险,需根据业务场景权衡。
额外优化建议
如果这个数量限制是长期业务规则,可以在数据库层面添加触发器,每次插入Game记录前自动校验用户的游戏数量,超过限制就抛出异常,从底层彻底保证数据一致性。
内容的提问来源于stack exchange,提问作者aqa
相关产品推荐
相关产品推荐

