处理Transactionscope后,Entity Framework和Transactionscope不会还原隔离级别

Run*_*e G 12 .net ado.net entity-framework transactions transactionscope

我对事务范围和实体框架有点兴奋.

最初,我们希望应用程序中的所有连接在读取数据时使用快照隔离级别,但在某些情况下,我们希望使用read committed或read uncommitted隔离级别读取数据,为此我们将使用事务范围来临时更改隔离级别查询(如此处和不同博客中的几篇帖子所述).

但问题是,当处理事务范围时,隔离仍然保留在连接上,这会引起相当多的问题.

我尝试了所有类型的变化,但结果相同; 隔离级别保留在事务范围之外.

是否有人可以为我解释这种行为或者可以解释我做错了什么?

我已经通过将事务范围封装在一个为我恢复隔离级别的一次性类中找到了解决问题的方法,但是我希望对这种行为有一个很好的解释,我认为这种行为不仅影响我的代码,而且其他人也是.

这是一个说明问题的示例代码:

using (var context = new MyContext())
{
    context.Database.Connection.Open();

    //Sets the connection to default read snapshot
    using (var command = context.Database.Connection.CreateCommand())
    {
        command.CommandText = "SET TRANSACTION ISOLATION LEVEL SNAPSHOT";
        command.ExecuteNonQuery();
    }

    //Executes a DBCC USEROPTIONS to print the current connection information and this shows snapshot
    PrintDBCCoptions(context.Database.Connection);

    //Executes a query
    var result = context.MatchTypes.ToArray();

    //Executes a DBCC USEROPTIONS to print the current connection information and this still shows snapshot
    PrintDBCCoptions(context.Database.Connection);

    using (var scope = new TransactionScope(TransactionScopeOption.Required,
        new TransactionOptions()
        {
            IsolationLevel = IsolationLevel.ReadCommitted //Also tried ReadUncommitted with the same result
        }))
    {
        //Executes a DBCC USEROPTIONS to print the current connection information and this still shows snapshot
        //(This is ok, since the actual new query with the transactionscope isn't executed yet)
        PrintDBCCoptions(context.Database.Connection);
        result = context.MatchTypes.ToArray();
        //Executes a DBCC USEROPTIONS to print the current connection information and this has now changed to read committed as expected                    
        PrintDBCCoptions(context.Database.Connection);
        scope.Complete(); //tested both with and without
    }

    //Executes a DBCC USEROPTIONS to print the current connection information and this is still read committed
    //(I can find this ok too, since no command has been executed outside the transaction scope)
    PrintDBCCoptions(context.Database.Connection);
    result = context.MatchTypes.ToArray();

    //Executes a DBCC USEROPTIONS to print the current connection information and this is still read committed
    //THIS ONE is the one I don't expect! I expected that the islation level of my connection should revert here
    PrintDBCCoptions(context.Database.Connection);
}
Run Code Online (Sandbox Code Playgroud)

Run*_*e G 21

好吧,经过一些挖掘,今天我发现了一点,我将分享调查结果,让其他人知道并获得意见和建议.

我的问题发生在几个原因取决于环境.

数据库服务器版:

首先,操作的结果取决于您运行的SQL Server版本(在SQL Server 2012和SQL Server 2014上测试).

SQL Server 2012

在SQL Server 2012上,最后设置的隔离级别将遵循后续操作的连接,即使它被释放回连接池并从其他线程/操作检索回来.在实践中; 这意味着如果您在某个线程/操作中将隔离级别设置为使用事务读取未提交,则连接将保留此直到另一个事务范围将其设置为另一个隔离级别(或通过执行SET TRANSACTION ISOLATION LEVEL命令)连接).不好,你可能会在不知情的情况下突然变脏.

例如:

Console.WriteLine(context.MatchTypes.Where(mt => mt.Id == 2).Select(mt => mt.LastUpdated).First());

using (var scope = new TransactionScope(TransactionScopeOption.Required, 
                                        new TransactionOptions 
                                        { 
                                            IsolationLevel = IsolationLevel.ReadUncommitted 
                                        }))
{
    Console.WriteLine(context.MatchTypes.Where(mt => mt.Id == 2)
                                        .Select(mt => mt.LastUpdated).First());
    scope.Complete(); //tested both with and without
}

Console.WriteLine(context.MatchTypes.Where(mt => mt.Id == 2).Select(mt => mt.LastUpdated).First());
Run Code Online (Sandbox Code Playgroud)

在此示例中,第一个EF命令将使用数据库缺省值运行,事务范围内的一个将使用ReadUncommitted运行,第三个EF命令也将使用ReadUncommitted运行.

SQL Server 2014

另一方面,在SQL Server 2014上,每次从连接池获取连接时,sp_reset_connection过程(似乎无论如何都是这样)都会将隔离级别设置回数据库的默认值,即使重新连接也是如此.来自同一交易范围.在实践中; 这意味着如果您有一个事务范围,您执行两个后续命令,则只有第一个命令将获得事务范围的隔离级别.也不好; 您将获得(基于数据库上的默认隔离级别)获取锁定或快照读数.

例如:

Console.WriteLine(context.MatchTypes.Where(mt => mt.Id == 2).Select(mt => mt.LastUpdated).First());

using (var scope = new TransactionScope(TransactionScopeOption.Required, 
                                        new TransactionOptions 
                                        { 
                                            IsolationLevel = IsolationLevel.ReadUncommitted 
                                        }))
{
    Console.WriteLine(context.MatchTypes.Where(mt => mt.Id == 2)
                             .Select(mt => mt.LastUpdated).First());
    Console.WriteLine(context.MatchTypes.Where(mt => mt.Id == 2)
                             .Select(mt => mt.LastUpdated).First());
    scope.Complete(); 
}
Run Code Online (Sandbox Code Playgroud)

在此示例中,第一个EF命令将使用数据库默认值运行,事务中的第一个将使用ReadUncommitted运行,但范围内的第二个将突然再次作为数据库默认运行.

手动打开连接问题:

在具有手动打开连接的不同SQL Server版本上会发生其他问题,但是,我们严格不需要这样做,所以我现在不打算深入研究这个问题.

使用Database.BeginTransaction:

出于某种原因,Entity Framework的Database.BeginTransaction逻辑似乎在两个数据库中都可以正常工作,但在我们的代码中,我们针对两个不同的数据库工作,然后我们需要事务范围.

结论:

在此之后我发现这种隔离级别的处理与SQL Server中的事务范围相结合非常错误,在我看来使用它并不安全,并且可能在我看到的任何应用程序中导致严重问题.使用它要非常谨慎.

但事实仍然是,我们需要在代码中使用它.在最近处理了MS的乏味支持而没有那么好的结果之后,我将首先找到适用于我们的解决方法.然后,我将使用Connect报告我的发现,并希望Microsoft在事务范围处理和连接方面做出一些最佳操作.

解:

解决方案(据我所知)是这样的.

以下是此解决方案的要求:1.由于其他应用程序针对需要此数据库的同一数据库运行,数据库必须处于隔离级别的READ COMMITTED,我们不能在数据库2上使用READ COMMITTED SNAPSHOT默认值.我们的应用程序必须具有缺省值为SNAPSHOT隔离级别 - 这是通过使用SET TRANSACTION ISOLATIONLEVEL SNAPSHOT来解决的.如果有事务范围,我们需要遵守这个的隔离级别

所以基于这些标准,解决方案将是这样的:

在上下文构造函数中,我注册到StateChange事件,当状态更改为Open并且没有活动事务时,使用经典ADO.NET将隔离级别默认为快照.如果使用事务范围,我们需要根据此处的设置运行SET TRANSACTION ISOLATIONLEVEL来遵循此设置(为了限制我们自己的代码,我们将只允许ReadConmitted,ReadUncommitted和Snapshot的IsolationLevel).至于Database.BeginTransaction在上下文中创建的事务,它似乎应该受到尊重,所以我们不对这些类型的事务做任何特殊操作.

以下是上下文中的代码:

public MyContext()
{
    Database.Connection.StateChange += OnStateChange;
}

protected override void Dispose(bool disposing)
{
    if(!_disposed)
    {
        Database.Connection.StateChange -= OnStateChange;
    }

    base.Dispose(disposing);
}

private void OnStateChange(object sender, StateChangeEventArgs args)
{
    if (args.CurrentState == ConnectionState.Open && args.OriginalState != ConnectionState.Open)
    {
        using (var command = Database.Connection.CreateCommand())
        {
            if (Transaction.Current == null)
            {
                command.CommandText = "SET TRANSACTION ISOLATION LEVEL SNAPSHOT";
            }
            else
            {
                switch (Transaction.Current.IsolationLevel)
                {
                    case IsolationLevel.ReadCommitted:
                        command.CommandText = "SET TRANSACTION ISOLATION LEVEL READ COMMITTED";
                        break;
                    case IsolationLevel.ReadUncommitted:
                        command.CommandText = "SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED";
                        break;
                    case IsolationLevel.Snapshot:
                        command.CommandText = "SET TRANSACTION ISOLATION LEVEL SNAPSHOT";
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            }

            command.ExecuteNonQuery();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我已经在SQL Server 2012和2014中测试了此代码,它似乎可以工作.它不是最好的代码,它有它的局限性(例如,每次EF执行都会对数据库执行SET TRANSACTION ISOLATIONLEVEL,从而增加额外的网络流量.)

  • OMG这是坚果 - 它仍然是EF6.1.3的当前状态吗? (3认同)