使用相同的 SqlConnection 对 SqlCommand.BeginExecuteNonQuery 的多个并发调用

Posted

技术标签:

【中文标题】使用相同的 SqlConnection 对 SqlCommand.BeginExecuteNonQuery 的多个并发调用【英文标题】:Multiple concurrent calls to SqlCommand.BeginExecuteNonQuery using same SqlConnection 【发布时间】:2011-09-16 12:34:14 【问题描述】:

我有一些工作 C# 代码使用 SqlConnection 创建临时表(例如,#Foo),调用存储的过程来填充这些临时表并将结果返回给 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();
        
    

【问题讨论】:

运行 SQLProfiler 看看实际发生了什么... 嗨,我确实运行了 SQLProfiler。在 SQL Server 中,对 sproc(更新临时表)的调用似乎总是按顺序发生,尽管不一定按照我使用 BeginExecuteNonQuery 调用 sproc 的顺序(顺序应该不重要)。在 C# 方面,对 BeginExecuteNonQuery 的调用是同时发生的,我相信是因为数据的序列化和传输 - sproc 采用表值参数,我将相应的 DataTable 传递给它。我担心并发的 SqlCommands 会混淆共享连接的一些非线程安全部分。 没有回答问题,但可能会在此处回避问题 - 您能否将 DataTable 中的数据客户端连接为作业循环的一部分,然后在所有工作都处理了吗?记忆可能是你唯一的障碍。 【参考方案1】:

好吧,冒着收到很多反对票的极端风险,我不得不对此发表评论。首先,这是一个很好的问题,并且很好地解决了您提到的具体潜在问题。但是,您忽略了讨论您要完成的这个“冗长”过程。

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

如果您提出的问题难以回答,请更改问题。

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

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

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

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

我今天工作的产品在数据库中有 2.4tb 的数据,我们没有单个 sproc、join 语句,甚至没有不相等的 where 子句。

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

“我问的问题对吗?”

【讨论】:

我希望我能给你的答案投票 100 次。我的答案之一收到了 -5,与您的类似:-) @Davita Thx,是的,像这样的答案经常被击败;)我想知道 OP 会做什么...... 数据库肯定不是用于存储/序列化的——如果这就是你使用它的全部,那么是的,正如你所说,你可能最好使用自己的数据结构。您提到了 B-Trees 和 ISAM,因此您必须意识到,RDBMS 只是这些以及无数其他(堆、bst、tst、散列等)的有效实现的主机,如果您的复杂问题可以减少到其中一项或多项,那么 RDBMS 实际上可能是您以最佳方式解决它的最佳方法。 @Gordy 为了避免关于什么是数据库的宗教辩论,我承认 RDBMS 可以非常有效地解决一些问题。还有很多东西不擅长。问题是人们不知道何时停止。业务逻辑被“吸入”到 SQL 中,就像一个黑洞从宇宙中吸光一样。我不是在谈论 select 语句,或者 group by,甚至是单个 SQL 语句。我说的是这样的废话:bit.ly/na3cSi【参考方案2】:

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

ConcurrentQueue in .NET 4: http://msdn.microsoft.com/en-us/library/dd267265.aspx Concurrency enable queue for .NET 3.5 (on Stack Overflow)

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

另一个想法,以防您的 ExecuteNonQuery 命令基本上是多个 INSERT: ExecuteNonQuery has an overload with a StringCollection 将多条sql语句作为一个操作发送。

【讨论】:

顺便说一句。小心不要让自己从每个连接中访问相同的对象时陷入僵局!【参考方案3】:

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

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

奇怪的是,您实际上获得了性能提升。这让我觉得您有大量数据,在发送到 SQL Server 时需要时间来处理。传输块时,由于数据传输和数据处理是同时进行的,因此消耗的时间更少。

无论如何,应该没有问题。

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

【讨论】:

【参考方案4】:

是的,非常好的问题。

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

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

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

问候

彼得

【讨论】:

谢谢,我们实际上是在使用 MARS(见上面的代码)。我一直在用 ILSpy 研究 SqlConnection,它肯定不是线程安全的,所以我可能不得不改变使用单个连接。我可以做的是为全局临时表创建一个唯一的表名,例如##Foo_Guid,并将表名传递给我的 procs。然后,我可以打开新的 SqlConnections 并仍然访问全局临时表,当最后一个 SqlConnection 关闭时,它将消失。全局临时表理论上每个人都是可见的,但是只有我的c#代码知道表名。 确实,我没有注意到。你为什么不去永久的桌子? 全局临时表会自动清理(尽管我可能会在完成我的过程后删除它们),我认为我应该获得与永久表接近或相同的性能。我正在尝试增量步骤,并尽量不改变数据库设计。 原谅我,但我不相信这是要走的路 :) 虽然它会起作用,但管理起来仍然很尴尬 - 你必须确保至少打开一个连接所以它们不会被删除。如果在表名中添加 guid,则意味着您将不得不使用动态 sql 对表执行操作,这可能会对性能产生负面影响。如果您出于审计/报告的原因收到存储数据的新要求,则必须将它们复制到永久表并编写单独的代码以从这些表中读取。这些只是我目前看到的几个原因。 感谢 cmets。我理解这些担忧。在我的例子中,这些表是真正临时的,只是用于一长串计算中的一些中间步骤。【参考方案5】:

这是一个问题,如果运行数据修改语句对您有帮助。 MARS 是多个活动结果集的首字母缩写词 - 结果集是 SELECTFETCH 语句的结果,在 .NET 中,它通常意味着您可以在同一连接上打开多个 DataReader。但是任何数据修改操作都被认为是原子的,并且必须在执行其他操作之前完成(或者可以继续从结果集中检索数据)-read about it here。所以我认为你的异步命令在并且仍然按顺序执行。

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

【讨论】:

【参考方案6】:

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

【讨论】:

谢谢,你描述的其实就是我在做的,即使用多个线程池线程。出于这个问题的目的,我简化为从一个线程多次使用 BeginExecuteNonquery。在任何一种情况下,我都使用一个 SqlConnection,并且该连接正被多个线程访问,这几乎可以肯定是一件坏事。 例如,我用 ILSpy 看到 SqlConnection 上有一个内部属性保存当前的 SqlCommand,SqlCommand 将此属性设置为 this 作为 BeginExecuteNonQuery 的一部分,并将在 EndExcuteNonQuery 中为空。当 SqlCommands 共享相同的 SqlConnection 并同时运行并交错时,这样的事情会影响 SqlConnection 的状态。【参考方案7】:

这绝对不安全。理论上它可能永远工作得很好,但它总是有失败的危险。更糟糕的是,由于它不受支持,它可能会以您没有注意到的方式失败,例如返回错误数据,而不是引发异常。

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

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

【讨论】:

【参考方案8】:

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

尝试移动线-

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

【讨论】:

以上是关于使用相同的 SqlConnection 对 SqlCommand.BeginExecuteNonQuery 的多个并发调用的主要内容,如果未能解决你的问题,请参考以下文章

SqlConnection 和 SqlDataReader 的复用

如何从 Sqlconnection 获取当前用户

TransactionScope 打破 SqlConnection 池?

使用 SQLConnection 连接到 SQL CE 数据库

C# SQLConnection 池

更新表 SQL 查询:SqlConnection