【问题标题】:SqlDependency subscription not working when using IsolationLevel.ReadUncommitted in (unrelated?) Transaction在(无关?)事务中使用 IsolationLevel.ReadUncommitted 时,SqlDependency 订阅不起作用
【发布时间】:2013-11-05 14:19:29
【问题描述】:

我已经设法让 SqlDependency 工作,但前提是我在我认为与 SqlDependency 无关的 SQL 事务中不使用 IsolationLevel.ReadUncommited

当我在事务中使用IsolationLevel.ReadUncommitted(下面有大量评论)时,SqlDependency 订阅失败,并立即发出OnChange 通知:

sqlNotificationEventArgs.Info = "Isolation";
sqlNotificationEventArgs.Source = "Statement";
sqlNotificationEventArgs.Type = "Subscribe";

当我删除 IsolationLevel 时,一切都按预期工作(当然,隔离不正确)。

这是我的相关代码:

private static string connString = "the connection string";
[MTAThread]
private static void Main(string[] args)
    while(true)
    {
        using (var context = new LinqDataContext(connString))
        {
            var conn = context.Connection;
            conn.Open();
            /***********************************************************************/
            /* Remove `IsolationLevel.ReadUncommitted` and the SqlDependency works */
            /***********************************************************************/
            using (var trans = conn.BeginTransaction(IsolationLevel.ReadUncommitted))
            {
                // simplified query, the real query uses UPDATE OUTPUT INSERTED
                const string sqlCommand = "SELECT [Columns] FROM dbo.[TABLE] WHERE [Status] = 'ready'";
                results = conn.Query({transaction: trans, sql: sqlCommand});
                trans.Commit();
            }
            DoAwesomeStuffWithTheResults(results, context);
        }
        WaitForWork();
    }
}

SqlDependency相关代码:

private static ManualResetEvent _quitEvent = new ManualResetEvent(false);

/// <summary>
/// Sets up a SqlDependency a doesn't return until it receives a Change notification
/// </summary>
private static void WaitForWork(){
    // in case we have dependency running we need to go a head and stop it first. 
    SqlDependency.Stop(connString);
    SqlDependency.Start(connString);

    using (var conn = new SqlConnection(connString))
    {
        using (var cmd = new SqlCommand("SELECT [Status] From dbo.[TABLE]", conn))
        {
            cmd.Notification = null;

            var dependency = new SqlDependency(cmd);
            dependency.OnChange += dependency_OnDataChangedDelegate;

            conn.Open();

            cmd.ExecuteReader();
        }
    }
    _quitEvent.WaitOne();
    SqlDependency.Stop(connString);
}
private static void dependency_OnDataChangedDelegate(object sender, SqlNotificationEventArgs e)
{
    ((SqlDependency)sender).OnChange -= dependency_OnDataChangedDelegate;
    _quitEvent.Set();
}

我觉得在设置 SqlDependency 之前,我已经正确处理了上下文、它的连接和事务,但似乎情况并非如此。

我在这里做错了什么?

【问题讨论】:

    标签: c# sql transactions sql-server-2012 sqldependency


    【解决方案1】:

    恭喜SqlDependency 工作(我一点也不讽刺,很多人在这方面都失败了)。

    现在是时候阅读 MSDN 上的 Creating a Query for Notification 主题了。您将看到查询对通知有效的条件,包括此要求:

    语句不得在 READ_UNCOMMITTED 或 SNAPSHOT 隔离级别下运行。

    我写了关于the basics of how SqlDependency works,也许会澄清一些误解。而且,作为一个侧节点,由于您使用的是 Linq,您可能对LinqToCache 感兴趣,它提供了Linq 查询和SqlDependency 之间的桥梁。

    另一条评论:不要Start()Stop() 你的SqlDependency nilly-willy。你很快就会后悔的。 Start() 应该只在应用启动期间调用一次,Stop() 在应用关闭期间只调用一次(严格来说,是在应用域加载和卸载期间)。

    现在,关于您的问题:重要的隔离级别是通知查询。这意味着,您附加订阅的查询,不是您执行 UPDATE 的查询(我不会评论在脏读下执行 UPDATE 的智慧......或@ 987654324@)。据我所知,您显示的代码不应在 read_uncommitted 下发布查询。在您发出SET TRANSACTION ISOLATION ... 之后,该会话中的所有后续事务(即所有语句)都将处于该隔离级别之下。您关闭连接(通过 DataContext 的处置),然后使用不同的连接。除非...您使用连接池。欢迎来到无辜受害者俱乐部:)。 Connection pooling leaks isolation level changes across Close()/Open() boundaries。那是你的问题。有一些简单的解决方案:

    在我们交谈时,您还需要阅读以下内容:Using Tables as Queues

    【讨论】:

    • 哈哈,谢谢;这是一段艰难的旅程!你是说一旦我用READ_UNCOMMITTED 建立了一个事务,所有进一步的语句都在那个隔离级别下运行?或者我没有正确处理交易(using 应该这样做,不是吗?)? (我承认我可能没有完全理解这里的一些核心 SQL 原则)
    • 在 IsolationLevel 设置为 IsolationLevel.Unspecified 的事务中运行 SqlDependency cmd 成功了!
    • 感谢您的编辑。我将通读所有内容并适当地更新我的代码。 :-)
    【解决方案2】:

    这是根据 Remus Rusanu 在他的回答中给出的提示的更新代码:

    private static string connString = "the connection string";
    [MTAThread]
    private static void Main(string[] args)
        // Start() is supposed to be called exactly once, during app startup
        // and Stop() exactly once during app shutdown:
        SqlDependency.Start(connString);
        AppDomain.CurrentDomain.ProcessExit += delegate
        {
            SqlDependency.Stop(connString);
        };
    
        while(true) // to infinity, and beyond.
        {
            using (var context = new LinqDataContext(connString))
            {
                var conn = context.Connection;
                // Connection pooling leaks isolation level changes across 
                // Close()/Open() boundaries, use TransactionScope to avoid this.
                using (var scope = CreateTransactionScope(TransactionScopeOption.Required, transactionOptions))
                {
                    conn.Open();
                    const string sqlCommand = "UPDATE TOP(1) [Table] SET [Status] = 'budy' OUTPUT INSERTED.[Column], */... MORE ...*/ WHERE [Status] = 'ready'";
                    results = conn.Query(sqlCommand);
                    scope.Complete();
                }
                DoAwesomeStuffWithTheResults(results, context);
            }
            WaitForWork();
        }
    }
    

    SqlDependency相关代码:

    /// <summary>
    /// Sets up a SqlDependency and doesn't return until it receives 
    /// a Change notification
    /// </summary>
    private static void WaitForWork(string connString)
    {
        var changedEvent = new AutoResetEvent(false);
        OnChangeEventHandler dataChangedDelegate = (sender, e) => changedEvent.Set();
        using (var conn = new SqlConnection(connString))
        {
            using (var scope = Databases.TransactionUtils.CreateTransactionScope())
            {
                conn.Open();
                var txtCmd = "SELECT [FileID] FROM dbo.[File] WHERE [Status] = 'ready'";
                using (var cmd = new SqlCommand(txtCmd, conn))
                {
                    var dependency = new SqlDependency(cmd);
                    OnChangeEventHandler dataChangedDelegate = null;
                    dataChangedDelegate = (sender, e) =>
                    {
                        dependency.OnChange -= dataChangedDelegate;
                        changedEvent.Set();
                    };
                    dependency.OnChange += dataChangedDelegate;
                    cmd.ExecuteScalar();
                }
                scope.Complete();
            }
        }
        changedEvent.WaitOne();
        dependency.OnChange -= dependencyOnDataChangedDelegate;
    }
    

    新的 TransactionScope 代码:

    /// <summary>
    /// Using {the default} new TransactionScope Considered Harmful
    /// http://blogs.msdn.com/b/dbrowne/archive/2010/06/03/using-new-transactionscope-considered-harmful.aspx
    /// </summary>
    private static TransactionScope CreateTransactionScope(System.Transactions.IsolationLevel isolationLevel = System.Transactions.IsolationLevel.ReadCommitted)
    {
        var transactionOptions = new TransactionOptions
        {
            IsolationLevel = isolationLevel,
            Timeout = TransactionManager.MaximumTimeout
        };
        return new TransactionScope(TransactionScopeOption.Required, transactionOptions);
    }
    

    【讨论】:

      猜你喜欢
      • 2017-10-18
      • 2020-03-24
      • 1970-01-01
      • 1970-01-01
      • 2013-04-07
      • 1970-01-01
      • 1970-01-01
      • 2017-08-12
      • 1970-01-01
      相关资源
      最近更新 更多