Параллельное выполнение запроса внутри общего OleDbConnection / OleDbTransaction

#c# #tsql #sql-server-2008 #oledb #task-parallel-library

#c# #tsql #sql-server-2008 #oledb #задача-параллельная-библиотека

Вопрос:

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

 //doesn't work
using (OleDbConnection oConn = TheDataAccessLayer.GetConnection())
using (OleDbTransaction oTran = oConn.BeginTransaction())
Parallel.ForEach(ORMObjects, (ORMObject, State) =>
{

        if (!State.ShouldExitCurrentIteration amp;amp; !State.IsExceptional)
        {
              var Error = ORMObject.SomethingThatExecutesANonQuery(oConn,oTran)

              if (Error.Number != 0)
                  State.Stop();

        }

});
  

Если я заблокирую соединение для ExecuteNonQuery, ошибки исчезнут, но производительность снизится.

  //works
    using (OleDbConnection oConn =  TheDataAccessLayer.GetConnection())
    using (OleDbTransaction oTran = oConn.BeginTransaction())
    Parallel.ForEach(ORMObjects, (ORMObject, State) =>
    {

            if (!State.ShouldExitCurrentIteration amp;amp; !State.IsExceptional)
            {
              lock(oConn)
              {
                    var Error = ORMObject.SomethingThatExecutesANonQuery(oConn,oTran)

                if (Error.Number != 0)
                      State.Stop();
             }

            }

    });
  

Предположим, что

  • Я не могу изменить характер ORM: SQL не может быть увеличен

  • Бизнес-правила требуют, чтобы взаимодействие выполнялось в рамках одной транзакции

Итак:

  • Есть ли более лучший / более эффективный способ распараллеливания взаимодействий OleDb?

  • Если нет, есть ли альтернатива OleDb-клиенту, которая может в полной мере использовать преимущества параллелизма? (Может быть, собственный клиент MSSQL?)

Ответ №1:

Транзакции должны быть ACID, но «долговечность» должна применяться только в конце транзакции. Таким образом, физический ввод-вывод на диск может быть отложен после очевидного выполнения инструкции SQL и фактически выполняться в фоновом режиме, пока ваша транзакция обрабатывает другие инструкции.

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

  • Выполните инструкцию SQL [A], которая записывает данные. Диск фактически не затрагивается, записи просто ставятся в очередь на потом, поэтому поток выполнения очень быстро возвращается клиенту (т. Е. [A] не блокируется надолго).
  • Выполните инструкцию SQL [B], которая записывает данные. Записи ставятся в очередь, и [B] не блокируется надолго, как и раньше. На данный момент физический ввод-вывод [A] может уже выполняться в фоновом режиме.
  • В транзакции выполняется другая обработка, в то время как СУБД выполняет физический ввод-вывод на диск в фоновом режиме.
  • Транзакция зафиксирована.
    • Если записи в очереди завершены, ждать не нужно.
    • Если к настоящему времени записи в очереди не завершены, подождите, пока они не завершатся. Кстати, некоторые базы данных могут ослабить требования к «долговечности», чтобы избежать этого ожидания, но не MS SQL Server (AFAIK).

Конечно, существуют сценарии, в которых этот «автопараллелизм» СУБД не будет работать хорошо, например, когда есть WHERE предложение, которое для разных операторов касается разных разделов на разных дисках — СУБД хотела бы распараллелить эти предложения, но не может, если они передаются в нее один за другим.

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


Кстати, MARS не поможет вам в распараллеливании ваших операторов — согласно MSDN: «Однако обратите внимание, что MARS определяется в терминах чередования, а не в терминах параллельного выполнения».

Комментарии:

1. Здесь много полезной информации. Боюсь, что с вашей точки зрения, в итоге я получаю яблоки с апельсинами: я могу сравнить 1 соединение и 1 транзакцию, выполняемую последовательно, с 80 000 подключений и транзакций параллельно (без объединения в пул). Между ними разница в 15 секунд (всего около полутора минут) В зависимости от того, насколько эффективно работает объединение, я надеялся на существенную экономию, если бы мог поддерживать одно и то же соединение.

2. Дополнение: приведенный выше сценарий с 1 подключением блокировкой выполняется примерно на 10 секунд быстрее, чем последовательный, и примерно на 5 секунд медленнее, чем «полная параллель»

Ответ №2:

Обнаружено, что OleDbConnection, похоже, не является потокобезопасным.

Да, это соответствует документации:

Любые общедоступные статические (общие в Visual Basic) элементы этого типа являются потокобезопасными. Не гарантируется потокобезопасность любых членов экземпляра.

Поэтому просто создайте соединение внутри потока и предоставьте базовому поставщику OLE DB обрабатывать объединение соединений. Также, если у вас есть возможность, обязательно избавьтесь от OleDbConnection и используйте соответствующий ADO.NET драйвер для вашей базы данных, и если вы не используете какую-то очень экзотическую базу данных, должен быть ADO.NET водитель.

Комментарии:

1. Мне нужно поделиться транзакцией — бизнес-правила диктуют, что все откатывается при сбое.

Ответ №3:

Поскольку это не потокобезопасно, измените значение Parallel.ForEach на нормальное foreach и выполняйте их последовательно. Лучше, чтобы он работал медленнее, чем вообще не работал.

Ответ №4:

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

Убедитесь, что у вас включен пул подключений и соответствующим образом заданы минимальные и максимальные свойства подключения.

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