在(无关?)Transaction中使用IsolationLevel.ReadUncommitted时,SqlDependency预订不起作用

时间:2013-11-05 14:19:30

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

我设法让SqlDependency工作,但只要我不使用IsolationLevel.ReadUncommited我认为是与SqlDependency无关的SQL事务。

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

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

当我删除I​​solationLevel时,一切都按预期工作(当然,隔离是不对的。)

以下是我的相关代码:

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之前,我觉得我已经正确处理了上下文,它的连接和事务,但看起来情况并非如此。

我在这里做错了什么?

2 个答案:

答案 0 :(得分:7)

祝贺SqlDependency工作(我根本没有讽刺,很多人都失败了。)

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

  

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

我写过关于the basics of how SqlDependency works的文章,也许会解决一些误解。而且,作为副节点,由于您使用的是Linq,因此您可能会对LinqToCache感兴趣,the wisdom of using dirty reads for anything会在Linq个查询与SqlDependency之间建立桥梁。

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

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

  • 您可以(必须!)在Open()
  • 之后明确重置隔离级别
  • 您可以使用System.Transactions范围(我的建议)。强制性阅读:Using Tables as Queues
  • 不要使用连接池。

在我们谈话时,您还需要阅读此内容:{{3}}。

答案 1 :(得分:1)

以下是基于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);
}