Подтвердить что ты не робот

Несколько одновременных вызовов в SqlCommand.BeginExecuteNonQuery с использованием того же SqlConnection

У меня есть рабочий код С#, который использует SqlConnection для создания временных таблиц (например, #Foo), вызывает хранимые procs для заполнения этих временных таблиц и возврата результатов клиенту С#, используйте С# для выполнения сложных вычислений по этим результатам, и использовать результаты расчета для обновления одной из созданных ранее временных таблиц.

Из-за временных таблиц, используемых во всем процессе, мы должны иметь только одно SqlConnection.

Я определил узкое место производительности при обновлении таблицы temp с результатами расчета. Этот код уже загружал обновления для предотвращения нехватки памяти у С# -клиента. Каждая партия вычисленных данных была отправлена ​​в сохраненный процесс через SqlCommand.ExecuteNonQuery, а sproc по очереди обновляет временную таблицу. Этот код тратил большую часть времени на этот вызов ExecuteNonQuery.

Итак, я поменял его на BeginExecuteNonQuery, а также на код, чтобы ждать по потокам и вызывать EndExecuteNonQuery. Это улучшило производительность примерно на треть, но меня беспокоит наличие нескольких одновременных вызовов в SqlCommand.BeginExecuteNonQuery с использованием того же SqlConnection.

Это нормально, или я буду сталкиваться с проблемами с потоками?

Извините за подробные объяснения.

В документах MSDN указано:

Метод BeginExecuteNonQuery возвращается немедленно, но пока код не выполнит соответствующий вызов метода EndExecuteNonQuery, он не должен выполнять никаких других вызовов, которые запускают синхронное или асинхронное выполнение с тем же объектом SqlCommand.

Это означает, что разные объекты SqlCommand могут вызывать BeginExecuteNonQuery до завершения первого SqlCommand.

Вот какой код, который иллюстрирует проблему:

    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();
        }
    }
4b9b3361

Ответ 1

Ну, в крайнем случае, когда я получаю много голосов, я должен прокомментировать это. Во-первых, это хороший вопрос и хорошо сказано для решения конкретной потенциальной проблемы, о которой вы говорили. Тем не менее, вы пренебрегли обсуждением этого "длительного" процесса, который вы пытаетесь выполнить.

Мой опыт подумал обо мне...

Если вопрос, который вы задаете, трудно ответить, измените вопрос.

Хотя я очень мало знаю о вашей конкретной проблеме, я думаю, что это аккуратно относится к вашему затруднительному положению. Как отмечали другие... Таблицы Temp неприятны, создание собственных таблиц для конкретной задачи еще более противно, обновление большого количества данных в SQL дорого.

Спросите себя: "Можете ли вы избежать всего этого?"

Довольно часто люди предпочитают внедрять чрезвычайно сложную логику в базах данных, потому что они убеждены, что SQL может сделать это быстрее. Практически это ошибочная концепция, Базы данных - устройства хранения/сериализации, они хорошо хранят, обновляют, локализуют и синхронизируют доступ к данным. Они недостаточно оснащены для обработки сложных операций. Даже после того, как Microsoft (и другие) удалила базу данных, вставив в нее полные языки разработки, она не может работать так же хорошо, как хорошо написанный клиент (* в зависимости от сложности операций, которые, как я подозреваю, вы превзошли).

В качестве примера у вас есть база данных с необработанными данными размером около 2 гб. Вы хотите создать сложный отчет или анализ всего набора данных. Ну просто просто 2gb памяти легко получить, slurp всей базы данных (или той части, которая вам нужна) в память с помощью словарей или что-то другое, чтобы создать поиск, который вам понадобится. В зависимости от нескольких факторов все это, скорее всего, будет работать в несколько раз быстрее, чем SQL, может быть легко протестировано и будет (IMHO) значительно легче создавать, отлаживать и поддерживать, чем неприятный ассортимент SPROC, создающих динамический SQL. Даже с более чем 2 гб необработанных данных клиентские кеши могут быть легко созданы с использованием нескольких существующих технологий (B-Trees, ISAM и т.п.).

Продукт, над которым я работаю сегодня, имеет 2,4 тб данных в базе данных, и у нас нет ни одного оператора sproc, join или даже предложения where без равенства.

Но, увы, мой совет может быть или не быть уместным для ваших конкретных обстоятельств, поскольку я не знаю ваших целей или ограничений. Надеюсь, если ничего другого, это заставит вас спросить себя:

"Я задаю правильный вопрос?"

Ответ 2

Вы можете использовать шаблон производителя-потребителя с двумя потоками и двумя одновременными, но независимыми соединениями sql.

У производителя (1-й поток) есть DataReader (соединение 1-го уровня) и записывается его результаты в очередь блокировки. Потребитель (2-й поток) читает из очереди, имеет ExecuteNonQuery (второе соединение sql) и записывает в таблицу (-ы) temp.

Другая идея в случае, если ваши команды ExecuteNonQuery в основном являются несколькими INSERT: ExecuteNonQuery имеет перегрузку с StringCollection, чтобы отправить несколько операторов sql как одну операцию.

Ответ 3

Может быть только один DataReader, связанный с объектом Command, и может существовать множество объектов Command, связанных с тем же соединением. Единственное, что вы не можете сделать здесь, это использовать ту же команду с различными параметрами.

Однако при запуске транзакции базы данных (подразумеваемой, если не явной) ресурсы, связанные с этой транзакцией, блокируются до тех пор, пока транзакция не будет завершена или не будет откатна, и все процессы, которые хотят запросить эти ресурсы, помещаются в очередь. SQL Server довольно хорошо управляет очередями. У меня было несколько проблем с взаимоблокировками из-за высокой загрузки сервера в SQL Server 2000, но таких проблем с более поздними версиями не было.

Странно, что вы действительно получили повышение производительности. Это заставляет меня думать, что у вас очень большой объем данных, что требует времени для обработки при отправке на SQL Server. При передаче блоков меньше времени потребляется, так как передача данных и обработка данных выполняются одновременно.

Во всяком случае, с этим не должно быть никаких проблем.

Однако рассмотрите возможность использования CLR Assemblies (если этот параметр доступен) для обработки информации непосредственно в механизме базы данных без TCP-трафика.

Ответ 4

Да, действительно хороший вопрос.

Возможно, вы могли бы использовать функцию, которая была введена в SQL Server 2005 под названием MARS: http://msdn.microsoft.com/en-us/library/ms345109(v=sql.90).aspx

MARS позволяет повторно использовать одно и то же соединение для чтения и записи, но у него есть некоторые ограничения, и, честно говоря, я не знаю никого, кто его использовал.

Из того, что я вижу, возможно, можно было бы рассмотреть вашу проблему из другой точки. Возможно, вместо использования временных таблиц и необходимости следить за ними через весь процесс, который в итоге должен быть синхронным - возможно, вы можете создать набор постоянных таблиц, который будет содержать дополнительный столбец JobId. Тогда вы не ограничены одним потоком. У вас может быть таблица, которая сохранит историю заданий. После того, как вы вставляете строку в эту таблицу, вы извлекаете scope_identity() и добавляете ее ко всем элементам вашего алгоритма. В таблицах может храниться более одной копии результатов за раз, и любые запросы, которые читают или обновляют данные, будут использовать идентификатор JobId в качестве установленного идентификатора. Если вы правильно индексируете таблицы, у вас будет очень плавный дизайн, который будет гораздо более масштабируемым, чем решение, которое вы пытаетесь реализовать сейчас.

Привет

Петр

Ответ 5

Это вопрос, который поможет вам в выполнении операторов модификации данных. MARS является акронимом множества активных наборов результатов. Результатом является результат операторов SELECT или FETCH, а в .NET это означает, что вы можете иметь несколько DataReaders, открытых в одном соединении. Но любая операция модификации данных считается атомной, и она должна завершиться до того, как будет выполнена другая операция (или извлечение данных из набора результатов может продолжаться) - читает об этом здесь. Поэтому я думаю, что ваши асинхронные команды находятся в и продолжают выполняться последовательно.

Вы можете использовать несколько соединений, если ваше основное соединение создает глобальную временную таблицу ##TempName вместо #Temp. Глобальная временная таблица должна быть видна из другого сеанса, в то время как основной сеанс все еще активен.

Ответ 6

Почему бы просто не запускать обе команды из двух асинхронных потоков, используя ExecuteNonQuery вместо BeginExecuteNonquery, и позволить пулу соединений работать в более традиционном смысле? Затем просто подождите в поточном пуле.

Ответ 7

Это определенно небезопасно. Теоретически она может работать навсегда, но она всегда будет в опасности. Хуже того, поскольку он не поддерживается, он может выйти из строя таким образом, который вы не заметите, например, при возврате плохих данных вместо исключения исключения.

Что MARS позволяет вам выполнять команды, связанные с соединением, в то время как в середине чтения набора результатов. Это полезно, например, если вы хотите обработать каждую строку результата и отправить обновления обратно в базу данных, не загружая сначала весь набор результатов в ваше приложение. Однако он НЕ позволяет одновременно отправлять несколько команд одному и тому же соединению.

В зависимости от ваших потребностей вы можете использовать MARS соответствующим образом для повышения производительности без многопоточности. В противном случае вам нужно будет использовать несколько соединений, для которых потребуются глобальные таблицы temp или постоянные таблицы. Даже тогда вам нужно быть осторожным, чтобы избежать взаимоблокировок, и спроектировать ваши запросы таким образом, чтобы блокировка не разрушала преимущества, которые вы пытаетесь получить из нескольких соединений.

Ответ 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();