Продолжается подписка на наши издания! Вы не забыли подписаться?

Разработка производительных .NET-приложений, взаимодействующих с СУБД

Опубликовано: 03.07.2006

Мы не раз обращались к теме повышения эффективности .NET-приложений, но до сих пор обсуждение касалось эффективности непосредственно кода, создаваемого программистом. Однако эффективность многих .NET-приложений во многом зависит от взаимодействия с СУБД и других внешних факторов. Поэтому в этот раз мы решили опубликовать статью, посвященную взаимодействию с СУБД и промежуточным уровнем. Если вы не читали предыдущих статей, посвященных повышению эффективности .NET-приложений, обратите внимание на то, что начиная оптимизировать программу в первую очередь нужно понимать, требуется ли оптимизация, и если да, то что именно нужно оптимизировать. В этом вам помогут профайлеры, причем в данном случае, кроме обычных профайлеров .NET-приложений полезно будет воспользоваться специализированных SQL-профайлеров, позволяющих оценить эффективность тех или иных операций с СУБД. Кроме того, начиная оптимизировать код взаимодействия с БД, стоит обратить внимание на то, является ли он узким местом в приложении. Для этого достаточно сравнить загрузку процессора сервером приложений (если таковой имеется), СУБД и сетевой трафик. Если сетевой трафик и нагрузка на СУБД невелики, а нагрузка на сервер приложений, напротив, велика, скорее всего, проблемы производительности вызваны неэффективностью алгоритмов, используемых сервером приложений, и оптимизация работы с БД ничего не даст. Если нагрузка на СУБД невелика, а трафик велик, то, скорее всего, приложение выбирает слишком много данных, и стоит заняться более тщательной фильтрацией данных на стороне СУБД. Если же загрузка процессора сервером БД велика, а трафик, напротив, невелик, то проблемы, скорее всего, кроются в неэффективных запросах, и в первую очередь стоит обратить внимание на запросы, производящие массовую агрегацию данных.

В общем, помните главное правило грамотной оптимизации – никогда не занимайтесь оптимизацией «на всякий случай». – прим.ред.

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

Примеры C#-кода, иллюстрирующие приемы оптимизации производительности, включают:

Введение

Разрабатывать ориентированные на производительность .Net-приложения непросто. Стандарт .Net включает только базовые принципы и опредления интерфейсов. Кроме того, провайдеры данных .Net не генерируют исключений, сообщающих, что их код исполняется слишком медленно.

Разработка .Net-приложений – это сложный процесс, частично потому, что код может сильно зависеть от провайдера данных. Если вы работаете с несколькими СУБД, вы обнаружите различия в концепциях программирования для разных провайдеров данных. Для создания эффективного приложения понадобится гораздо больше знаний о СУБД.

Этот материал содержит рекомендации, выработанные при исследовании реализаций поставляемых .Net-приложений и анализе распространенных ошибок, относящихся к вопросам производительности. Ниже обсуждаются вопросы выбора .NET-объектов и методов, дизайна .NET-приложений, выборки и обновления данных.

Соблюдение этих правил поможет вам решить многие распространенные проблемы производительности .NET-систем.

Дизайн .NET-приложений

Использование пулов подключений

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

Пулы подключений – это часть провайдера данных .NET. Пулы подключений позволяют повторно использовать имеющиеся подключения. Закрытие подключения не разрывает физическое соединение с БД. Когда приложению нужно подключение, повторно используется имеющееся подключение, исключая сетевой ввод/вывод, нужный для создания нового подключения.

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

В следующем примере C#-кода создаются три новых объекта OracleConnection, но для работы с ними нужны только два пула подключений. Заметьте, что первая и вторая строки подключения отличаются только значениями User ID, Password и Max Pool Size.

OracleConnection conn1 = new OracleConnection(); 
conn1.ConnectionString = "Service Name=ORCL;Host=Accounting; "
  + "User ID=scott;password=tiger;Max Pool Size=100";
 
conn1.Open(); 
// Пул А создан.

OracleConnection conn2 = new OracleConnection(); 
conn2.ConnectionString = "Service Name=ORCL;Host=Accounting; "
  + "User ID=lucy;password=quake;Max Pool Size=200"; 
 
conn2.Open(); 
// Создан пул В, так как строки подключения различаются. 

OracleConnection conn3 = new OracleConnection(); 
conn3.ConnectionString = "Service Name =ORCL;Host=Accounting; "
+ "User ID=scott;password=tiger;Max Pool Size=100"; 
 
conn3.Open(); 
// conn3 назначено существующее подключение из пула А. 

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

Обработку подключений и SQL-команд нужно продумывать до реализации.

Открытие и закрытие подключений

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

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

Закрывайте подключения в блоке finally. Код в блоке finally выполняется всегда, невзирая на возможные исключения. Это гарантирует явное закрытие подключений. Например:

try  
{ 
    DBConn.Open(); 
    ... // Всякие интересные вещи 
} 
catch (Exception ex)  
{ 
    // Обработка исключений 
} 
finally 
{ 
    //  Закрытие подключения 
    if (DBConn != null) 
       DBConn.Close(); 
} 

При использовании пулов подключений их открытие и закрытие – недорогие операции. Использование метода Close() объекта Connection провайдера данных возвращает подключение в пул. Помните, однако, что закрытие подключения автоматически закрывает все ассоциированные с подключением объекты DataReader.

Управление фиксацией транзакций

Фиксация транзакций медленна из-за дискового или сетевого ввода/вывода. Всегда начинайте транзакцию после подключения; иначе вы окажетесь в режиме autocommit.

Что фактически участвует в фиксации? Сервер БД должен сбросить на диск все страницы данных, содержащие измененные или новые данные. Обычно это последовательная запись в файл журнала, но, тем не менее, это дисковый ввод/вывод. По умолчанию при подключению к источнику данных включается autocommit. Этот режим обычно снижает производительность из-за существенного объема дискового ввода/вывода, необходимого для фиксации каждой операции.

Кроме того, некоторые серверы БД не имеют родного режима autocommit. Для этого типа серверов провайдер данных .NET должен явно выдавать выражения COMMIT и BEGIN TRANSACTION для каждого действия на сервере. Кроме большого объема дискового ввода/вывода, нужного для поддержки режима autocommit, происходит падение производительности из-за трех сетевых обращений, выполняемых приложением для каждого выражения.

Следующий код начинает транзакцию в Oracle:

OracleConnection MyConn = new OracleConnection 
              ("Connection String info"); 
MyConn.Open() 
 
// Начало транзакции 
OracleTransaction TransId = MyConn.BeginTransaction(); 
 
// Команды, вовлеченные в данную транзакцию 
OracleCommand OracleToDS = new OracleCommand(); 
OracleToDS.Transaction = TransId; 
...  
// Продолжение транзакции 

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

Выбор правильной транзакционной модели

Многие системы поддерживают распределенные транзакции, то есть, транзакции, использующие несколько подключений. Распределенные транзакции минимум в четыре раза медленнее обычных из-за логирования и сетевого обмена, нужных для коммуникации между всеми компонентами, вовлеченными в распределенную транзакцию (провайдером данных .NET, монитором транзакций и СУБД). Распределенные транзакции нужно использовать только если транзакция должна использовать несколько СУБД или серверов.

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

Использование команд, выбирающих мало данных, или не выбирающих их вовсе

Такие команды, как INSERT, UPDATE и DELETE не возвращают данных. Используйте эти команды с методом ExecuteNonQuery объекта Command. Хотя эти команды можно успешно выполнить, используя метод ExecuteReader, провайдер данных .NET должным образом оптимизирует доступ к БД для выражений INSERT, UPDATE и DELETE только через метод ExecuteNonQuery.

Следующий пример показывает, как вставить строку в таблицу EMPLOYEES с помощью ExecuteNonQuery:

DBConn.Open(); 
DBTxn = DBConn.BeginTransaction(); 
 
// Задаем значение свойства Connection объекта Command
cmd.Connection = DBConn; 

// Задаем текст Command в операторе INSERT
cmd.CommandText = "INSERT into EMPLOYEES VALUES (15,'HAYES','ADMIN',6,  
'17-APR-2002',18000,NULL,4)"; 
// Задаем транзакционное свойство объекта Command
cmd.Transaction = DBTxn; 
 
// Исполняем выражение через ExecuteNonQuery, 
// поскольку мы не возвращаем результатов
cmd.ExecuteNonQuery(); 
// Фиксируем транзакцию
DBTxn.Commit(); 
 
// Закрываем подключение
DBConn.Close(); 

Используйте метод ExecuteScalar объекта Command, чтобы получить из БД одиночное значение, например, сумму или итог. Метод ExecuteScalar возвращает только значение первой колонки первой строки набора результатов. Опять же, для исполнения таких запросов можно использовать метод ExecuteReader, но использование метода ExecuteScalar говорит провайдеру данных .NET провести оптимизацию для набора результатов, состоящего из одной строки и одной колонки. Тем самым провайдер данных может избежать излишней нагрузки и улучшить производительность. Следующий пример показывает, как получить число членов группы:

// Получение числа сотрудников из таблицы EMPLOYEES 

// Открытие подключения к БД Sybase
SybaseConnection  Conn; 
Conn = new SybaseConnection("host=bowhead;port=4100;User ID=test01;  
Password=test01;Database Name=Accounting"); 
Conn.Open(); 
 
// Создание объекта Command 
SybaseCommand  salCmd = new SybaseCommand("SELECT count(sal) FROM” +  
"EMPLOYEES WHERE sal>’50000’",Conn); 
 
try 
{ 
    int count = (int)salCmd.ExecuteScalar(); 
} 
catch (Exception ex)  
{ 
    // Вывод исключений 
    MessageBox.Show (ex.Message); 
} 
// Закрытие подключения 
Conn.Close(); 

Использование команд несколько раз

Выбор, использовать или нет метод Command.Prepare, может иметь существенное положительное (или отрицательный) эффект на производительность выполнения запроса. Метод Command.Prepare говорит нижележащему провайдеру данных оптимизировать множественное исполнение выражений, использующих маркеры параметров. Заметьте, что Prepare можно использовать с любой командой, независимо от того, какой метод используется (ExecuteReader, ExecuteNonQuery или ExecuteScalar).

Рассмотрим случай, когда провайдер данных .NET реализует Command.Prepare путем создания на сервере хранимой процедуры, содержащей подготовленное выражение. Создание хранимых процедур влечет за собой существенные накладные расходы, но выражение может исполняться множество раз. При всей накладности создания хранимых процедур расходы на исполнение выражения минимальны, поскольку запрос уже разобран и пути оптимизации сохранены при создании процедуры. Приложения, исполняющие одно выражение насколько раз, могут существенно выиграть от использования Command.Prepare.

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

Использование "родных" управляемых провайдеров

Связи с неуправляемым кодом неблагоприятно влияют на производительность. Вызов неуправляемого кода из управляемого заставляет CLR производить дополнительные проверки этих вызовов.

.NET CLR – очень эффективная и оптимизированная среда. Использование исключительно управляемого кода дает возможность воспользоваться для улучшения производительности приложений многочисленными встроенными службами. CLR предоставляет автоматическое управление памятью, так что разработчикам не приходится тратить время на поиски утечек памяти. Автоматическое управление сроками жизни объектов включает сборку мусора, возможности масштабирования и поддержку множественных версий. Кроме того, безопасность .NET Framework позволяет защитить код и данные от повреждения или неправильного использования другим кодом. Администратор может определить политику безопасности, назначая права на уровне предприятия, машины, сборки или пользователя.

Однако многие провайдеры данных .NET для обеспечения сетевых соединений с серверами БД вынуждены служить мостами между CLR и внешним кодом. В текущей версии CLR работа с такими мостами медленна и привносит непроизводительные издержки.

В зависимости от архитектуры вы можете не понимать, что нижележащий провайдер данных .NET служит причиной угроз безопасности и потерь производительности. Будьте внимательными при выборе .NET-провайдера, рекламируемого как полностью управляемый. Всего несколько вендоров выпускают провайдеры данных, действительно написанные на управляемом коде и реализующие весь стек в виде управляемого компонента.

Выбор .NET-классов и методов

...

Заключение

Продуманные дизайн и реализация увеличивают производительность .NET-приложений. Запрашивая только необходимые данные, выбирая функции, оптимизирующие производительность, используя управляемый код, а также правильно управляя подключениями, транзакциями и обновлением записей, вы добьетесь повышения эффективности приложений и уменьшения сетевого трафика.

........................
С полным содержанием данной статьи можно ознакомиться в печатной версии журнала

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

Copyright © 1994-2016 ООО "К-Пресс"