2017-03-04 46 views
5

我想知道與sp_getapplock和會話所有者類型一起使用EF上下文的風險/安全性。基於docs,我的理解是會話所有者意味着鎖定將在會話結束時釋放,如果在此之前沒有明確釋放。我假設這也意味着連接結束。使用會話擁有的SQL Server sp_getapplock與EF6 DbContexts安全嗎?

我已經使用了事務擁有的所有鎖,但是它帶來了其他問題和複雜性,所以我想知道如何使用會話擁有的鎖執行此操作,以及如何與操作衝突或不會與操作衝突DbContext及其連接處理。

我不是100%確定默認情況下DbContext是如何工作的,無論它是使用池還是每個上下文實例打開並關閉自己的連接(我讀過的文檔似乎暗示了後者)。對於這個問題,假設我沒有做任何連接管理(我很少這樣做),所以EF正在處理它,或者處理它的任何處理。

如果我創建一個DbContext實例,搶連接,執行SQL創建會話資鎖,使用上下文爲正常,釋放鎖和上下文的處置,將這項工作是否正確? (在現實中,這將是在IDisposable包裝,以防止這一點,但仍然是問題。)

爲了說明不佳:

using (var ctx = new MyContext()) { 
    var conn = ctx.Database.Connection.Open(); 
    conn.ExecuteSqlSomehow("sp_getapplock blahblah"); 
    try { 
     // Lots of queries, savechanges, etc. 
    } finally { 
     // Oops I forgot to conn.ExecuteSql("sp_release the lock"); 
    } 
} 

await WatchMovieAsync(); 

using (var ctx = new MyContext()) { 
    // Can this reuse the same connection, session and/or lock? 
} 

問題:

  1. 能另一種情況下以某種方式重新在處理實例後使用我的連接和會話鎖定?
  2. 對於這個問題,另一個上下文(例如在另一個線程中)是否可以使用我在其上發出鎖的連接,而原始線程仍在using塊之內?
  3. EF可以關閉鎖定的連接,並在同一個using區塊(例如SaveChanges)內打開另一個鎖定連接,從而釋放鎖定?

回答

0

答案似乎是否定的。顯然,主要的問題是,只要從池中重新使用池連接(不是釋放回池時),應該釋放任何基於會話的鎖,就會調用sp_reset_connection。 This social.msdn link聲稱sp_reset_connection甚至在EF命令之間調用。

最簡單的解決方案是使用專用連接,該連接在鎖的持續時間內保持打開狀態,如果API可能會交錯任何sp_resest_connection調用(分析可以證明這一點),則不用於其他SQL。

另見SqlConnection vs Sql Session. Do their lifetimes coincide?

what-does-exec-sp-reset-connection-mean-in-sql-server-profiler

下面是一個C#sp_GetAppLock()的包裝,我們正試圖(免責聲明 - 不徹底的測試):

用法:

using (new globalApplicationSqlServerLock(connectionString, "theUniqueLockName") 
{ 
    // protected code 
} 

Implmentation:

using System; 
using System.Data; 
using System.Data.SqlClient; 

namespace CompanyNamespace.Server.DataAdaptersCommon 
{ 
    /// <summary> 
    /// A wrapper around sp_GetAppLock (SQL Server), useful for global locking (by arbitrary name) across multiple machines. 
    /// For instance: Include the compatibility version number within the lock resource to create a version specific lock. 
    /// </summary> 
    public class GlobalApplicationSqlServerLock : SimplifiedDisposableBase 
    { 
     /// <summary> 
     /// Last returned value from sp_GetAppLock() or sp_ReleaseAppLock() 
     /// </summary> 
     public Int32 LastReturnCode { get; private set; } 

     /// <summary> 
     /// The SQL Connection to use. 
     /// </summary> 
     readonly SqlConnection _connection; 

     /// <summary> 
     /// The name of the lock chosen by the callse 
     /// </summary> 
     readonly string _lockName; 

     /// <summary> 
     /// The cumulative times that Lock() has been called. 
     /// </summary> 
     int _lockCount; 

     // Refer to sp_GetAppLock() 
     // 
     const string _lockOwner = "session"; 
     const string _lockMode = "Exclusive"; 
     const string _dbPrincipal = "public"; 

     /// <summary> 
     /// Wait a maximum of this many seconds. 
     /// </summary> 
     Int32 _waitForLockMaxSeconds; 

     /// <summary> 
     /// Constructor accepting a Connection String 
     /// </summary> 
     /// <param name="connectionString">Connection string should include "...;AppName=AppType,WebPid" to improve DB side logging.</param> 
     /// <param name="lockName"></param> 
     /// <param name="waitForLockMaxSeconds">Throw an exception if the lock cannot be acquired within this time period.</param> 
     /// <param name="lockNow">True to obtain the lock via the contructor call. Lock is always released in Dipose()</param> 
     /// <param name="excludeFromOpenTransactionScope">True to exclude from any open TransactionScope</param> 
     public GlobalApplicationSqlServerLock(
      string connectionString, 
      string lockName, 
      Int32 waitForLockMaxSeconds = 30, 
      bool lockNow = true, 
      bool excludeFromOpenTransactionScope = true) 
     { 
      SqlConnectionStringBuilder conStrBuilder = new SqlConnectionStringBuilder(connectionString); 

      if (excludeFromOpenTransactionScope) 
       conStrBuilder.Enlist = false; 

      _waitForLockMaxSeconds = waitForLockMaxSeconds; 

      // The lock must use a dedicated connection that stays open for the duration of the lock. 
      // Otherwise, since the lock owner is "session", when the connection is closed the lock "may" be 
      // released since connection pooling could have inconsistent side effects. So caller must ensure 
      // lock is released (using IDisposable, etc). 
      // 
      _connection = new SqlConnection(conStrBuilder.ConnectionString);   
      _connection.Open(); 
      _lockName = lockName; 

      if (lockNow) 
       Lock(); 
     } 

     /// <summary> 
     /// Lock 
     /// </summary> 
     public void Lock() 
     { 
      string errMsg; 
      if (!TryLock(out errMsg)) 
       throw new Exception(errMsg); 
     } 

     /// <summary> 
     /// Try lock 
     /// </summary> 
     /// <param name="errMsg"></param> 
     /// <returns>True if lock obtained, false if not with error message.</returns> 
     public bool TryLock(out string errMsg) 
     {      
      using (SqlCommand command = new SqlCommand()) 
      { 
       command.Connection = _connection; 
       command.CommandType = CommandType.StoredProcedure; 

       command.CommandText = "sp_GetAppLock"; 
       command.Parameters.Add(new SqlParameter("@Resource", SqlDbType.NVarChar, 255) { Value = _lockName }); 
       command.Parameters.Add(new SqlParameter("@LockMode", SqlDbType.NVarChar, 32) { Value = _lockMode }); 
       command.Parameters.Add(new SqlParameter("@LockOwner", SqlDbType.NVarChar, 32) { Value = _lockOwner }); 
       command.Parameters.Add(new SqlParameter("@LockTimeout", SqlDbType.Int) { Value = _waitForLockMaxSeconds }); 
       command.Parameters.Add(new SqlParameter("@DBPrincipal", SqlDbType.NVarChar, 128) { Value = _dbPrincipal }); 
       command.Parameters.Add(new SqlParameter("@Result", SqlDbType.Int) { Direction = ParameterDirection.ReturnValue }); 

       command.ExecuteNonQuery(); 

       LastReturnCode = (int)command.Parameters["@Result"].Value; 
      } 

      switch (LastReturnCode) 
      { 
       case 0: 
       case 1: 
        _lockCount++; 
        errMsg = null; 
        return true; 
       case -1: 
        errMsg = "The lock request timed out."; 
        break; 
       case -2: 
        errMsg = "The lock request was canceled."; 
        break; 
       case -3: 
        errMsg = "The lock request was chosen as a deadlock victim."; 
        break; 
       case -999: 
        errMsg = "Indicates a parameter validation or other call error."; 
        break; 
       default: 
        errMsg = "Unexpected return value"; 
        break; 
      } 

      return false;    
     } 

     /// <summary> 
     /// Release the lock 
     /// </summary> 
     public void Release() 
     { 
      string errMsg; 

      using (SqlCommand command = new SqlCommand()) 
      { 
       command.Connection = _connection; 
       command.CommandType = CommandType.StoredProcedure; 

       command.CommandText = "sp_ReleaseAppLock"; 
       command.Parameters.Add(new SqlParameter("@Resource", SqlDbType.NVarChar, 255) { Value = _lockName }); 
       command.Parameters.Add(new SqlParameter("@LockOwner", SqlDbType.NVarChar, 32) { Value = _lockOwner }); 
       command.Parameters.Add(new SqlParameter("@DBPrincipal", SqlDbType.NVarChar, 128) { Value = _dbPrincipal }); 
       command.Parameters.Add(new SqlParameter("@Result", SqlDbType.Int) { Direction = ParameterDirection.ReturnValue }); 

       command.ExecuteNonQuery(); 
       LastReturnCode = (int)command.Parameters["@Result"].Value; 
      } 

      switch (LastReturnCode) 
      { 
       case 0: 
        _lockCount--; 
        return; 
       case -999: 
        errMsg = "Indicates a parameter validation or other call error."; 
        break; 
       default: 
        errMsg = "Unexpected return value"; 
        break; 
      } 

      throw new Exception(errMsg); 
     } 

     /// <summary> 
     /// Disposable implmentation 
     /// </summary> 
     protected override void FreeManagedResources() 
     {    
      try 
      { 
       while (_lockCount > 0) 
        Release(); 
      } 
      finally 
      { 
       try 
       { 
        if (_connection != null && _connection.State != ConnectionState.Closed) 
         _connection.Close(); 
       } 
       finally 
       { 
        base.FreeManagedResources(); 
       } 
      }    
     } 
    } 
} 


using System; 
using System.Collections.Generic; 
using System.Diagnostics; 

namespace CompanyNamespace.Common 
{ 
    /// <summary> 
    /// To support IDisposable, pass true to constructor and call: 
    /// 
    ///   AutoDispose(IDisposable) for each disposable at time of creation, 
    ///   
    /// Or override these as needed: 
    /// 
    ///   FreeManagedResources() and 
    ///   FreeUnmanagedResources() 
    ///   
    /// Multi-thread safe. 
    /// </summary> 
    public abstract class SimplifiedDisposableBase : IDisposable 
    { 
     /// <summary> 
     /// Flag for IDisposable 
     /// </summary> 
     protected bool _isDisposed = false; 

     /// <summary> 
     /// List of items that should be Dispose() when the instance is Disposed() 
     /// </summary> 
     private List<IDisposable> _autoDisposables = new List<IDisposable>(); 

     /// <summary> 
     /// Constructor 
     /// </summary> 
     public SimplifiedDisposableBase() 
     { 
     } 

     /// <summary> 
     /// Finalizer (needed for freeing unmanaged resources and adds a check a Dispose() check for managed resources). 
     /// </summary> 
     ~SimplifiedDisposableBase() 
     { 
      // Warning: An exception here will end the application. 
      // Do not attempt to lock to a possibly finalized object within finalizer 
      // http://stackoverflow.com/questions/4163603/why-garbage-collector-takes-objects-in-the-wrong-order    

      string errMessages = string.Empty; 
      try 
      { 
       errMessages = String.Format("Warning: Finalizer was called on class '{0}' (base class '{1}'). " + 
        "IDisposable's should usually call Dispose() to avoid this. (IsDisposed = {2})", 
        GetType().FullName, 
        typeof(SimplifiedDisposableBase).FullName, 
        _isDisposed); 

       Debug.WriteLine(errMessages); 

       Dispose(false);  // free any unmanaged resources     
      } 
#if DEBUG 
      catch (Exception ex) 
      { 
       errMessages = "Fatal: Exception occurred within Finalizer ~" + GetType().FullName + "()." + errMessages; 
       Debug.WriteLine(errMessages + " " + ex.Message); 

       // Verified that this exception appears in Windows Event Log and includes the originating class message and StackTrace[0] 
       throw new Exception(errMessages, ex); 
      } 
#else 
      catch (Exception) 
      { 
       /* Don't exit the application */ 
      } 
#endif 
     } 

     /// <summary> 
     /// Add an managed item to be automatically disposed when the class is disposed. 
     /// </summary> 
     /// <param name="disposable"></param> 
     /// <returns>The argument</returns> 
     public T AutoDispose<T>(T disposable) where T : IDisposable 
     { 
      lock (_autoDisposables) 
       _autoDisposables.Add(disposable); 

      return disposable; 
     }   

     /// <summary> 
     /// Derived class can override and chain for support of IDisposable managed resources. 
     /// </summary> 
     protected virtual void FreeManagedResources() 
     { 
      lock (_autoDisposables) 
      { 
       _autoDisposables 
        .ForEach(d => d.Dispose()); 

       _autoDisposables.Clear(); 
      } 
     } 

     /// <summary> 
     /// Derived class can optionally override for support of IDisposable unmanaged resources. 
     /// </summary> 
     protected virtual void FreeUnmanagedResources() { } 

     /// <summary> 
     /// Standard IDisposable Implmentation 
     /// </summary> 
     public void Dispose() 
     { 
      Dispose(true); // calling multiple times is okay 
      GC.SuppressFinalize(this); // http://stackoverflow.com/questions/12436555/calling-suppressfinalize-multiple-times is okay 
     } 

     /// <summary> 
     /// Dispose 
     /// </summary> 
     protected virtual void Dispose(bool isDisposing)   
     {    
      if (!isDisposing) // if called from finalizer, do not use lock (causes exception) 
      { 
       if (!_isDisposed) 
       { 
        FreeUnmanagedResources(); // always free these     
        _isDisposed = true; 
       } 
       return; 
      } 

      // Remainder is called from IDisposable (not finalizer) 

      // Based on "Implemenent IDisposable Correctly" 
      // http://msdn.microsoft.com/en-us/library/ms244737.aspx 

      lock (_autoDisposables) 
      { 
       if (_isDisposed) 
        return; // the docs specifically state that Dispose() must be callable multiple times without raising an exception 

       try 
       { 
        try 
        { 
         FreeManagedResources(); 
        } 
        finally 
        { 
         FreeUnmanagedResources(); // always free these 
        } 
       } 
       finally 
       { 
        _isDisposed = true; 
       } 
      } 
     }  
    }  
} 
+0

'sp_reset_connection'確實每次都調用,與Linq2Sql一樣。但是如果有一個活動的'TransactionScope',它不會*發生。 – GSerg