Dot-Net

實體框架和事務範圍在處理事務範圍後不會恢復隔離級別

  • February 11, 2015

我在事務範圍和實體框架方面有點掙扎。

最初我們希望應用程序中的所有連接在讀取數據時都使用快照隔離級別,但在某些情況下,我們希望讀取具有已送出讀或未送出讀隔離級別的數據,為此我們將使用事務範圍來臨時更改隔離級別用於查詢(如這裡的幾篇文章和不同部落格中所指出的)。

但是,問題是當事務範圍被釋放時,隔離仍然保留在連接上,這引起了相當多的問題。

我嘗試了所有類型的變體,但結果相同;隔離級別保留在事務範圍之外。

有沒有人可以為我解釋這種行為或可以解釋我做錯了什麼?

通過將事務範圍封裝在為我恢復隔離級別的一次性類中,我找到了解決該問題的方法,但我希望對此行為有一個很好的解釋,我認為這種行為不僅會影響我的程式碼,而且其他人也是。

這是一個說明問題的範常式式碼:

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

好吧,經過今天的一些探勘,我發現了一點,我將分享這些發現,以供其他人了解並獲得意見和建議。

我的問題發生取決於環境有幾個原因。

數據庫伺服器版本:

首先,操作的結果取決於您執行的 SQL Server 版本(在 SQL Server 2012 和 SQL Server 2014 上測試)。

SQL Server 2012

在 SQL Server 2012 上,最後設置的隔離級別將在後續操作中跟隨連接,即使它被釋放回連接池並從其他執行緒/操作中檢索回來。在實踐中; 這意味著如果您在某個執行緒/操作中將隔離級別設置為使用事務讀取未送出,則連接將保留此狀態,直到另一個事務範圍將其設置為另一個隔離級別(或通過在聯繫)。不好,你可能會在不知不覺中突然得到臟讀。

例如:

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 執行,第三個也將以 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 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,從而增加額外的網路流量。)

引用自:https://stackoverflow.com/questions/28442558