You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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;
    }
}

请问为何代码未按预期工作?是不是因为判断条件不属于数据库逻辑,导致事务无法生效?


问题原因及解决方案

核心原因

你的代码失效主要有两个关键点:

  1. 事务隔离级别限制:EF Core默认用数据库的默认隔离级别(比如SQL Server是READ COMMITTED),这个级别下,事务内查询到的count是事务启动时的快照数据,延迟期间其他事务插入的新记录不会被当前事务感知,所以判断条件依然成立,最终还是会插入数据。
  2. 非原子性操作:计数查询和插入是两个独立步骤,中间的延迟给了其他操作插入数据的窗口,即使加了事务,这两步也不是原子执行的,无法保证并发场景下的限制生效。

可行解决方案

要解决问题,必须把计数判断和插入操作变成数据库层面的原子操作,以下是两种常用方案:

方案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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 19:08:18