Entity Framework 并且 Transactionscope 在处理 Transactionscope 后不会恢复隔离级别

Entity Framework and Transactionscope doesn't revert the isolation level after dispose of Transactionscope

我在处理事务范围和 entity framework 方面遇到了一些困难。

最初我们希望应用程序中的所有连接在读取数据时都使用快照隔离级别,但在某些情况下,我们希望读取具有已提交读或未提交读隔离级别的数据,为此我们将使用事务范围来临时更改查询的隔离级别(正如此处和不同博客中的几篇文章所指出的)。

但是,问题是当处理事务范围时,隔离仍然保留在连接上,这会导致很多问题。

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

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

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

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

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

嗯,经过今天的一些挖掘,我发现了一些关于这个的东西,我将分享发现,让其他人知道并获得意见和建议。

我的问题发生的原因有多种,具体取决于环境。

数据库服务器版本:

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

SQL 服务器 2012

在 SQL Server 2012 上,最后设置的隔离级别将在后续操作中跟随连接,即使它被释放回连接池并从其他 threads/actions 检索回来。在实践中;这意味着如果您在某些 thread/action 中使用事务将隔离级别设置为未提交读取,则连接将保留它直到另一个事务范围将其设置为另一个隔离级别(或通过执行 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,第三个也将 运行 与读取未提交。

SQL 服务器 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 服务器版本上会发生其他问题,但是,我们绝对不需要这样做,所以我现在不打算深入探讨这个问题.

使用Database.BeginTransaction:

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

结论:

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

但事实仍然存在,我们需要在我们的代码中使用它。最近在 MS 处理了繁琐的支持,结果不是很好,我将首先找到适合我们的解决方法。然后,我将使用 Connect 报告我的发现,并希望 Microsoft 能最好地围绕事务范围处理和连接采取一些行动。

解法:

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

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

遵守隔离级别

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

在上下文构造函数中,我注册到 StateChange 事件,当状态更改为 Open 并且没有活动事务时,我又将隔离级别默认为使用经典 ADO.NET 的快照。如果使用事务作用域,我们需要根据此处的设置运行ning 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,从而增加额外的网络流量。)