使用同一SqlConnection对SqlCommand.BeginExecuteNonQuery进行多个并发调用

本文关键字:调用 并发 BeginExecuteNonQuery SqlCommand SqlConnection | 更新日期: 2023-09-27 17:59:24

我有一些正在使用的C#代码,它使用SqlConnection创建临时表(例如#Foo),调用存储的proc来填充这些临时表并将结果返回到C#客户端,使用C#对这些结果执行复杂的计算,并使用计算结果更新之前创建的一个临时表。

由于在整个过程中使用了临时表,因此我们必须只有一个SqlConnection。

在用计算结果更新临时表时,我发现了一个性能瓶颈。此代码已经在批处理更新,以防止C#客户端内存不足。每批计算的数据都通过SqlCommand.ExecuteNonQuery发送到存储过程,存储过程依次更新临时表。代码的大部分时间都花在了对ExecuteNonQuery的调用上。

因此,我将其更改为BeginExecuteNonQuery,以及等待线程和调用EndExecuteNonQuery的代码。这将性能提高了约三分之一,但我担心使用相同的SqlConnection同时调用SqlCommand.BeginExecuteNonQuery。

这可以吗,还是我会遇到线程问题?

抱歉解释太长。

MSDN文档状态:

BeginExecuteNonQuery方法会立即返回,但在代码执行相应的EndExecuteNonQuery方法调用之前,它不得执行任何其他调用来启动对同一SqlCommand对象的同步或异步执行。

这似乎意味着不同的SqlCommand对象可以在第一个SqlCommand完成之前调用BeginExecuteNonQuery。

以下是一些说明问题的代码:

    private class SqlCommandData
    {
        public SqlCommand Command { get; set; }
        public IAsyncResult AsyncResult { get; set; }
    }
    public static void TestMultipleConcurrentBeginExecuteNonQueryCalls(string baseConnectionString)
    {
        var connectionStringBuilder = new SqlConnectionStringBuilder(baseConnectionString)
                                          {
                                              MultipleActiveResultSets = true,
                                              AsynchronousProcessing = true
                                          };
        using (var connection = new SqlConnection(connectionStringBuilder.ConnectionString))
        {
            connection.Open();
            // ELIDED - code that uses connection to do various Sql work
            SqlDataReader dataReader = null;
                // in real code, this would be initialized from calls to SqlCommand.ExecuteReader, using same connection
            var commandDatas = new List<SqlCommandData>();
            var count = 0;
            const int maxCountPerJob = 10000;
            while (dataReader.Read())
            {
                count++;
                // ELIDED - do some calculations on data, too complex to do in SQL stored proc
                if (count >= maxCountPerJob)
                {
                    count = 0;
                    var commandData = new SqlCommandData
                                          {
                                              Command = new SqlCommand {Connection = connection}
                                          };
                    // ELIDED - other initialization of command - used to send the results of calculation back to DB
                    commandData.AsyncResult = commandData.Command.BeginExecuteNonQuery();
                    commandDatas.Add(commandData);
                }
            }
            dataReader.Close();
            WaitHandle.WaitAll(commandDatas.Select(c => c.AsyncResult.AsyncWaitHandle).ToArray());
            foreach (var commandData in commandDatas)
            {
                commandData.Command.EndExecuteNonQuery(commandData.AsyncResult);
                commandData.Command.Dispose();
            }
            // ELIDED - more code using same SqlConnection to do final work
            connection.Close();
        }
    }

使用同一SqlConnection对SqlCommand.BeginExecuteNonQuery进行多个并发调用

好吧,冒着获得大量反对票的极端风险,我不得不对这一点发表评论。首先,这是一个很好的问题,很好地解决了你提到的具体潜在问题。然而,你忽略了讨论你试图完成的这个"漫长"的过程。

我的经历让我想到了一件事。。。

如果你提出的问题很难回答,那就换个问题。

虽然我对你的具体问题知之甚少,但我认为这完全适用于你的困境。正如其他人提到的。。。临时表很糟糕,为特定任务创建自己的表更糟糕,在SQL中更新大量数据也很昂贵。

问问自己"你能避免这一切吗?"

很多时候,人们选择在数据库中实现极其复杂的逻辑,因为他们相信SQL可以更快地实现。实际上,这是一个有缺陷的概念,数据库是存储/序列化设备,它们擅长存储、更新、定位和同步对数据的访问。他们没有很好地处理复杂的操作。即使在微软(和其他公司)通过向数据库中注入完整的开发语言对数据库进行了破坏之后,它也无法像一个写得很好的客户端那样实现最佳性能(*取决于操作的复杂性,我怀疑你已经超越了它)。

例如,您有一个包含大约2 GB原始数据的数据库。您希望对整个数据集生成复杂的报告或分析。简单地放入2gb的内存很容易获得,使用字典或任何东西将整个数据库(或您需要的部分)拖到内存中,以创建您需要的查找。根据几个因素的不同,整个过程的运行速度可能是SQL的几倍,可以很容易地进行单元测试,并且(IMHO)将比构建动态SQL的各种糟糕的SPROC更容易构建、调试和维护。即使原始数据超过2gb,也可以使用几种现有技术(B-Trees、ISAM等)轻松创建客户端缓存。

我今天工作的产品在数据库中有2.4tb的数据,我们没有一个存储过程、联接语句,甚至没有一个不相等的where子句。

但遗憾的是,我的建议可能与你的具体情况有关,也可能不相关,因为我不知道你的目标或限制。希望,如果没有别的,它会让你问自己:

"我问的问题对吗?"

您可以使用具有2个线程和2个同时但独立的sql连接的生产者-消费者模式。

  • .NET 4中的并发队列:http://msdn.microsoft.com/en-us/library/dd267265.aspx
  • .NET 3.5的并发启用队列(堆栈溢出)

生产者(第一个线程)拥有DataReader(第一个sql连接),并将其结果写入阻塞队列。使用者(第二个线程)从队列中读取,具有ExecuteNonQuery(第二次sql连接)并写入临时表。

另一个想法是,如果ExecuteNonQuery命令基本上是多个INSERT:ExecuteNonQuery有一个带有StringCollection的重载,用于将多个sql语句作为一个操作发送。

一个命令对象只能有一个DataReader,并且可以有多个命令对象与同一连接关联。这里唯一不能做的就是使用具有不同参数的同一命令。

但是,当启动数据库事务(如果不是显式的,则是隐式的)时,与该事务关联的资源将被锁定,直到事务被提交或回滚,并且所有想要查询这些资源的进程都将被放入队列中。SQL Server可以很好地管理队列。由于SQL server 2000中的服务器负载很高,我遇到了一些死锁问题,但在以后的版本中没有这样的问题。

奇怪的是,你居然得到了绩效提升。这让我觉得您有大量的数据,在发送到SQL Server时处理这些数据需要时间。当传输块时,由于数据传输和数据处理是同时执行的,因此消耗的时间更少。

不管怎样,这应该没有任何问题。

但是,请考虑使用CLR程序集(如果此选项可用)直接在数据库引擎中处理信息,而不使用TCP通信。

是的,真的是个好问题。

也许您可以使用SQLServer2005中引入的一个名为MARS的功能:http://msdn.microsoft.com/en-us/library/ms345109(v=sql.90).aspx

MARS允许重复使用相同的连接进行读写,但它有一些局限性,坦率地说,我不知道有谁会使用它

不过,就我所见,也许可以从另一个角度来看待你的问题。也许,您可以创建一组包含额外列JobId的永久表,而不是使用临时表并在整个过程中一直关注它们,这最终必须是同步的。这样您就不会被约束到单个线程。你可以有一个表,将保持历史的工作。一旦向该表中插入一行,就可以检索scope_identity()并将其添加到算法的所有元素中。这些表一次可以保存多个结果副本,任何读取或更新数据的查询都将使用JobId作为集合标识符。如果您对表进行了正确的索引,您将拥有非常流畅的设计,它将比您现在尝试实现的解决方案更具可扩展性。

问候

Piotr

如果运行数据修改语句对您有帮助,这是一个问题。MARS是多个活动结果集的缩写-结果集是SELECTFETCH语句的结果,在.NET中,这通常意味着可以在同一连接上打开多个DataReader。但是,任何数据修改操作都被视为原子操作,必须先完成,然后才能执行其他操作(或者可以继续从结果集中检索数据)-请阅读此处。因此,我认为您的异步命令位于,并且仍然按顺序执行。

如果主连接创建全局临时表##TempName而不是#Temp,则可以使用多个连接。当主会话仍处于活动状态时,全局临时表应从其他会话可见。

为什么不使用ExecuteNonQuery而不是BeginExecuteNonquery从两个异步线程请求中运行这两个命令,并让连接池以更传统的方式解决问题?然后在线程池上等待。

这绝对不安全。理论上,它可能永远运行良好,但它永远有失败的危险。更糟糕的是,由于它不受支持,它可能会以一种你没有注意到的方式失败,比如返回坏数据,而不是抛出异常。

MARS允许您在读取结果集的过程中对连接执行命令。例如,如果您希望处理结果的每一行并将更新发送回数据库,而不需要首先将整个结果集加载到应用程序中,那么这将非常有用。但是,它不允许您同时向同一连接发送多个命令。

根据您的需要,您可以适当地使用MARS来提高性能,而无需多线程。否则,您将需要使用多个连接,这将需要使用全局临时表或永久表。即便如此,您也需要小心避免死锁,并在设计查询时确保锁定不会破坏您试图从多个连接中获得的优势。

我不确定这个问题是否仍然重要,但。。。

尝试移动线路-

connection.Open();

从使用开始,到新的sqlCommand之后,但在BeginExecuteNonQuery之前。。像这样-

                var commandData = new SqlCommandData
                                      {
                                          Command = new SqlCommand {Connection = connection}
                                      };
                connection.Open();
                // ELIDED - other initialization of command - used to send the results of calculation back to DB
                commandData.AsyncResult = commandData.Command.BeginExecuteNonQuery();