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

时间:2015-02-10 21:51:59

标签: .net entity-framework transactions ado.net 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);
}

1 个答案:

答案 0 :(得分:20)

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

有几个原因导致我的问题依赖于环境。

数据库服务器版本:

首先,操作的结果取决于您运行的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());

在此示例中,第一个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(); 
}

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

手动打开连接问题:

在手动打开连接的不同SQL Server版本上还会出现其他问题,但是,我们严格不需要这样做,所以我现在不打算解决这个问题。

使用Database.BeginTransaction:

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

<强>结论:

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

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

<强>解决方案:

解决方案(据我所知)就是这样。

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

的隔离级别

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

在上下文构造函数中,我注册到StateChange事件,当状态更改为Open并且没有活动事务时,使用经典ADO.NET将隔离级别默认为快照。如果使用事务范围,我们需要根据此处的设置运行SET TRANSACTION ISOLATIONLEVEL来遵循此设置(为了限制我们自己的代码,我们将只允许ReadCommitted,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();
        }
    }
}

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