Entity Framework 6未忽略enlist=false的TransactionScope问题咨询
我之前也踩过类似的坑,EF6.2对TransactionScope的处理逻辑和NHibernate确实不一样——哪怕你在连接字符串里设了enlist=false,它也不会完全忽略环境事务,这就是你遇到所有问题的核心原因。
疑问1:为什么没触发超时异常或SqlException,反而提示找不到元素?
要搞清楚这个问题,得先明确两个关键逻辑:
TransactionScope的默认超时是30秒,超时后它会自动回滚所有关联的事务,而不是直接抛出超时异常给你的查询操作。- 即使你设置了
enlist=false,EF6.2检测到环境中存在TransactionScope时,会自动为DbContext的操作创建一个本地事务,并把这个本地事务的生命周期和外层TransactionScope绑定——只有调用scope.Complete()时,本地事务才会提交;如果超时或者没调用Complete(),事务就会回滚。
你的执行流程其实是这样的:
- 第一个
SaveChanges()把数据写入了数据库,但这个操作处于EF创建的未提交本地事务中。 - 你在同一个
TransactionScope里用新连接查询,此时事务还没提交,加上TransactionScope默认的Serializable隔离级别,查询会一直等待锁释放。 - 30秒后
TransactionScope超时,自动回滚了那个本地事务,数据被删除。 - 此时等待中的查询终于执行,但数据已经不存在,所以抛出“找不到元素”的
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




