You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Entity Framework 6未忽略enlist=false的TransactionScope问题咨询

EF6.2 with TransactionScope and enlist=false: Unexpected locking, rollback, and missing element exception

我之前也踩过类似的坑,EF6.2对TransactionScope的处理逻辑和NHibernate确实不一样——哪怕你在连接字符串里设了enlist=false,它也不会完全忽略环境事务,这就是你遇到所有问题的核心原因。

疑问1:为什么没触发超时异常或SqlException,反而提示找不到元素?

要搞清楚这个问题,得先明确两个关键逻辑:

  • TransactionScope的默认超时是30秒,超时后它会自动回滚所有关联的事务,而不是直接抛出超时异常给你的查询操作。
  • 即使你设置了enlist=false,EF6.2检测到环境中存在TransactionScope时,会自动为DbContext的操作创建一个本地事务,并把这个本地事务的生命周期和外层TransactionScope绑定——只有调用scope.Complete()时,本地事务才会提交;如果超时或者没调用Complete(),事务就会回滚。

你的执行流程其实是这样的:

  1. 第一个SaveChanges()把数据写入了数据库,但这个操作处于EF创建的未提交本地事务中。
  2. 你在同一个TransactionScope里用新连接查询,此时事务还没提交,加上TransactionScope默认的Serializable隔离级别,查询会一直等待锁释放。
  3. 30秒后TransactionScope超时,自动回滚了那个本地事务,数据被删除。
  4. 此时等待中的查询终于执行,但数据已经不存在,所以抛出“找不到元素”的InvalidOperationException,而不是超时或SqlException——因为查询本身是在事务回滚后正常执行的,只是没找到目标数据。

疑问2:为什么数据库行被锁定,SSMS也无法查询?

还是因为EF自动创建的那个本地事务:

  • 第一个SaveChanges()执行时,EF在本地事务中插入数据,这行数据会被加上排他锁(符合Serializable隔离级别的锁机制)。
  • 在事务未提交或回滚前,其他会话(包括你的SSMS查询)在默认隔离级别(Read Committed)下无法读取被锁定的行,必须用NOLOCK提示才能读到未提交的脏数据。
  • 哪怕你设置了enlist=false,这个本地事务依然存在——因为EF是主动适配环境TransactionScope的,这和连接是否登记到分布式事务(DTC)没有关系。

为什么预期没达成,以及解决方案?

你以为enlist=false会让EF完全忽略TransactionScope,但EF6.2的逻辑是:只要存在环境TransactionScope,不管连接是否登记到DTC,EF都会将DbContext的操作绑定到本地事务,并跟随TransactionScope的生命周期提交/回滚——这和NHibernate严格遵守enlist=false配置的行为完全不同,也是你遗漏的关键逻辑。

要实现你的预期(数据持久化、无行锁、EF忽略TransactionScope),可以用以下两种方案:

方案1:用嵌套的TransactionScopeOption.Suppress屏蔽环境事务

在需要脱离外层事务的代码块外面套一个Suppress级别的事务范围,这样EF就不会创建绑定到外层TransactionScope的本地事务了:

using(var scope = new TransactionScope(TransactionScopeOption.Required, transactionScopeTimeout, TransactionScopeAsyncFlowOption.Enabled)) { 
    const string ConnectionString="server=localhost;username=;password=;enlist=false;Initial Catalog=blubber";

    // 用Suppress屏蔽外层事务,让EF操作不受影响
    using(var suppressScope = new TransactionScope(TransactionScopeOption.Suppress))
    {
        using(var myContext = new MyContext(ConnectionString)) { 
            var myTestObject = new TestObjectBuilder().WithId(1).Build(); 
            myContext.Add(myTestObject); 
            myContext.SaveChanges(); // 这里的操作会立即提交,不受外层TransactionScope控制
        }
        suppressScope.Complete();
    }

    // 数据已经提交,直接查询即可找到
    using(var myContext = new MyContext(ConnectionString)) { 
        var myObj = myContext.MyObjects.Single(s => s.Id == 1); 
    } 
}

方案2:手动控制EF的本地事务

直接在DbContext中开启独立的本地事务并手动提交,完全脱离外层TransactionScope的控制:

using(var scope = new TransactionScope(TransactionScopeOption.Required, transactionScopeTimeout, TransactionScopeAsyncFlowOption.Enabled)) { 
    const string ConnectionString="server=localhost;username=;password=;enlist=false;Initial Catalog=blubber";

    using(var myContext = new MyContext(ConnectionString)) { 
        // 开启EF独立的本地事务
        using(var dbTransaction = myContext.Database.BeginTransaction())
        {
            var myTestObject = new TestObjectBuilder().WithId(1).Build(); 
            myContext.Add(myTestObject); 
            myContext.SaveChanges();
            dbTransaction.Commit(); // 手动提交,不受外层TransactionScope影响
        }
    }

    using(var myContext = new MyContext(ConnectionString)) { 
        var myObj = myContext.MyObjects.Single(s => s.Id == 1); 
    } 
}

最后补充

EF6.2的这个设计是为了让开发者更方便地在TransactionScope中使用EF,哪怕不需要分布式事务,但它确实会和enlist=false的字面预期产生冲突。NHibernate的处理更贴近连接字符串配置的原意,这也是为什么你用NHibernate测试正常的原因。

内容的提问来源于stack exchange,提问作者Link

火山引擎 最新活动