45794 (665138), страница 3
Текст из файла (страница 3)
Начнем с DML, идея которых, в общем-то, должна быть очевидна. Допустим, у нас есть очень большая таблица (Very_Big_Table), для отчетов по которой надо периодически считать некие агрегатные значения. Поскольку таблица очень большая, то агрегаты считаются очень долго. Отчет не всегда должен быть актуальным, но всегда – согласованным, и строиться должен максимально быстро. Это значит, что в идеале агрегаты должны быть посчитаны заранее. Делать пересчет данных в обычном триггере накладно для операций обновления, так как расчет агрегатов происходит долго, как уже было упомянуто. И тут на помощь приходит Service Broker. В обычном триггере на изменение Very_Big_Table создается диалог (строго говоря, мало что мешает создать диалог заранее, разве что проблемы с запоминанием метки при развертывании) и отправляется сообщение, о том что таблица изменилась. Это занимает минимум времени, а изменяющий процесс идет дальше заниматься своими делами. Получатель же начинает не торопясь пересчитывать эти занудные агрегаты, чтобы к моменту, когда понадобится отчет, все уже было готово.
Вот как это может выглядеть. Сначала создадим необходимые тестовые таблички:
CREATE TABLE Very_Big_Table(ID int IDENTITY, Data bigint, [Time] DateTime) GO -- заполним таблицу данными -- INSERT INTO Very_Big_Table(Data, [Time]) SELECT object_id, create_date FROM sys.objects GO -- табличка для вычисленного агрегата -- CREATE TABLE Big_Aggregate(Agg bigint, [Time] DateTime) GO -- Ну и проинициализируем ее -- INSERT INTO Big_Aggregate(Agg, [Time]) SELECT Sum(Data), GetDate() FROM Very_Big_Table |
Теперь триггер на изменение очень большой таблички. Здесь мы сильно мудрствовать не будем, воспользуемся уже готовыми метаданными из предыдущего примера:
CREATE TRIGGER AsyncAggregate ON Very_Big_Table FOR INSERT, UPDATE, DELETE AS DECLARE @convHandler uniqueidentifier BEGIN DIALOG @convHandler FROM SERVICE [SourceService] TO SERVICE 'TargetService' ON CONTRACT [TestContract]; SEND ON CONVERSATION @convHandler MESSAGE TYPE [TestType] (N'The data hase been changed') END CONVERSATION @convHandler GO |
Передавать в сообщении никакой ценной информации нам не надо, так как принимающая сторона должна просто узнать, о том, что таблица поменялась, а признаком этого служит сам факт доставки сообщения. Более того, в данной ситуации нет необходимости даже вызывать команду SEND, так как закрытие диалога (END CONVERSATION) вызывает посылку специального сообщения об этом печальном событии на принимающую сторону. Однако в реальной ситуации может понадобиться передать некоторую информацию, и если ее необходимо структурировать, то придется воспользоваться XML.
Теперь займемся принимающей стороной. Для начала создадим процедуру пересчета агрегата:
CREATE PROCEDURE AggRecalculate AS -- очистка очереди -- RECEIVE * FROM [TargetQueue] -- небольшая задержка для имитации действительно долгого расчета -- WAITFOR DELAY '00:00:02' UPDATE Big_Aggregate SET Agg = (SELECT SUM(Data) FROM Very_Big_Table), [Time] = GetDate() GO |
Процедура готова, но есть одна проблема. Как выполнить эту процедуру при появлении сообщения в очереди? Конечно, можно, как и раньше, обернуть RECEIVE в WAITFOR, но в этом случае кто-то должен запусить процедуру, чтобы она начала ждать сообщений из очереди. И мало того, сообщение-то у нас может быть не одно. Значит, нужно чтобы после получения кто-то активизировал процедуру снова. Другими словами, нужен некий монитор, который следил бы за состоянием очереди и при появлении в ней сообщений вызывал нашу процедуру. К счастью, все уже сделано за нас. Такой монитор имеется в Service Broker, и для его включения достаточно немного изменить параметры очереди, указав, какую процедуру надо вызвать при получении сообщения:
ALTER QUEUE [TargetQueue] WITH ACTIVATION( STATUS = ON, PROCEDURE_NAME = AggRecalculate, MAX_QUEUE_READERS = 1, EXECUTE AS OWNER) |
Ключевое слово здесь, конечно же, ACTIVATION, то есть активация. Однако если параметр STATUS у нее выставлен в OFF, она не сработает. Как несложно догадаться, в параметре PROCEDURE_NAME указывается имя процедуры, которая будет вызвана при активации, а в EXECUTE AS – от имени какого пользователя эта процедура будет вызвана. Параметр MAX_QUEUE_READERS определяет максимальное количество процедур, которое одновременно может быть запущено для разгребания очереди. Если во время работы процедуры поступили новые сообщения, то запускается еще один экземпляр этой процедуры, и так до максимального разрешенного количества или опустошения очереди.
Теперь все готово для эксперимента, можно приступать. Сначала обновим нашу «очень_большую_таблицу», и тут же заберем данные из таблички агрегатов, затем подождем чуть-чуть и снова заберем агрегированные данные, чтобы увидеть, как они изменились после работы процедуры перерасчета, автоматически запущенной Service Broker-ом.
UPDATE Very_Big_Table SET Data = Data + 10 WHERE ID=1 SELECT * FROM Big_Aggregate WAITFOR DELAY '00:00:05' SELECT * FROM Big_Aggregate -- Результат: -- Agg Time -------------------- ----------------------- 76577545551 13:44:37.987 76577545561 13:59:24.630 |
Как можно заметить, все отлично сработало, произошло асинхронное обновление агрегатной таблицы. Вся прелесть в том, что агрегатная таблица может находиться на совершенно другом сервере в другой стране, надо только настроить подключение соответствующим образом, что совсем не сложно.
Асинхронные DDL и SQL-Trace триггеры (Event Notification)
Для реализации асинхронных триггеров на DDL-операции и события профайлера существует специальный механизм, Event Notification (извещение о событии).
ПРИМЕЧАНИЕ Надо учитывать, что в связи с асинхронностью данного механизма породившие это извещение изменения в базе или на сервере, не отменятся в случае отката извещения, как это было бы в DDL-триггере. Они – уже свершившийся факт. И еще один нюанс: поскольку события профайлера работают вне транзакций, то даже если изменение на сервере, вызвавшее посылку сообщения, не увенчается успехом, то само сообщение все равно будет доставлено до получателя, однако для DDL-событий это не работает, так как DDL-операции работают в рамках транзакции и в случае отмены DDL транзакции сообщение отправлено не будет. |
Как не сложно догадаться, этот механизм отслеживает события, на которые есть подписчики, и посылает соответствующее сообщение. Для того чтобы механизм сообщений заработал, достаточно создать очередь и сервис получателя с предопределенным контрактом [http://schemas.microsoft.com/SQL/Notifications/PostEventNotification], все остальное - и контракт, и диалог, и сервис с очередью отправителя, уже реализовано. Затем надо создать объект EventNotification, связывающий нужное событие с сервисом – и готово. На практике, допустим, для асинхронного аудита подключений к серверу и отключений от оного, это может выглядеть следующим образом:
-- сначала создадим очередь получателя, при желании -- здесь можно назначить процедуру обработки новых сообщений -- CREATE QUEUE [LoginQueue] GO -- затем необходимо создать сервис со специальным контрактом, -- в котором уже есть необходимые типы сообщений -- CREATE SERVICE [LoginService] ON QUEUE [LoginQueue]( [http://schemas.microsoft.com/SQL/Notifications/PostEventNotification]) GO -- Ну а теперь можно создать и сам Event Notification, связывающий -- серверные события с сервисом доставки сообщения -- CREATE Event Notification auditLogin ON SERVER FOR Audit_Login, Audit_Logout TO SERVICE 'LoginService', 'current database' |
Здесь ‘current database’ – это константа, которая говорит о том, что в качестве механизма доставки будет использоваться экземпляр Service Broker-а, установленный в текущей базе. Указание этого экземпляра является необходимым параметром при создании уведомления.
Вот, в общем-то, и все. Теперь осталось только получить красивый XML из очереди после очередного входа/выхода из системы и придумать, что с ним делать. Получить его можно все тем же, уже знакомым способом:
RECEIVE cast(message_body as xml) FROM [LoginQueue] |
Сам XML представляет собой результат вызова той же самой функции Eventdata(), что используется и в DDL-триггерах.
Комбинируя рассмотренную функциональность, можно добиться довольно причудливого поведения, например, активируя очередь, по таймеру проверять загрузку процессора и занимаемую сервером память. И если нагрузка на сервер достаточно низка, то отсылать другое сообщение, которое приведет к запуску долгой расчетной процедуры...
Асинхронные возможности клиентских приложений
Теперь самое время рассмотреть подробнее, какие возможности для работы с базой данных в асинхронном режиме есть у клиентского приложения.
Как можно было убедиться из предыдущих примеров, отправить задание на выполнение кому-то другому – это полдела, надо еще вовремя получить извещение о том, что асинхронная операция закончена. При разработке механизмов взаимодействия клиента с сервером этому вопросу было уделено должное внимание.
Асинхронное выполнение запросов
Помимо выполнения асинхронных операций на сервере и работы с очередями, в ADO.Net 2.0 добавлена специальная функциональность по асинхронной работе с БД со стороны клиента. Эта функциональность поддерживается только провайдером SqlClient (OleDB и остальные его не поддерживают). Зато (приятная новость) со стороны сервера жестких ограничений нет, и асинхронные запросы будут работать с SQL Server от Microsoft, начиная с седьмой версии, при условии, что режим работы с ними – не Shared Memory, а операционная система – Windows 2000/XP/2003.
Строго говоря, и в предыдущей версии Framework-а организация асинхронной обработки данных не была такой уж большой проблемой. Однако при этом приходилось выделять дополнительный поток и блокировать его в ожидании выполнения запроса. Для клиентских приложений это не представляет большой проблемы, но для серверных решений, вынужденных обслуживать множество клиентов одновременно, это может послужить источником неприятностей. Вся же прелесть данной реализации заключается в том, что дополнительный поток не создается. Вместо этого для достижения должного эффекта используются возможности асинхронного сетевого ввода/вывода. Вместо того, чтобы создавать новый поток и заставлять его ждать синхронной операции ввода/вывода для отправки запроса в БД и получения ответа, используются асинхронные возможности сетевого протокола Windows 2000/XP/2003 (с этим и связаны ограничения на использование ОС и режима Shared Memory для версий сервера ниже SQL 2005), позволяющие одному потоку отослать запрос и идти дальше по своим делам.
Для выполнения запросов в асинхронном режиме разработчики добавили несколько методов, однако придерживались минималистской политики, и добавили лишь самые необходимые методы. Поэтому далеко не все синхронные варианты Execute* обзавелись асинхронными аналогами, точнее, только три из них. Это ExecuteReader, получивший BeginExecuteReader и EndEsecuteReader, ExecuteNonQuery, получивший BeginExecuteNonQuery и EndExecuteNonQuery, и ExecuteXmlReader, получивший, как не сложно догадаться BeginExecuteXmlReader и EndExecuteXmlReader. Предполагается следующая схема применения этого богатства: метод Begin* получает все входные параметры и передает их для исполнения серверу, оставляя после себя потоку на память лишь некий объект, реализующий специальный интерфейс по имени IAsyncResult. Этот интерфейс может быть использован для отслеживания состояния выполнения операции. Из метода же End* с помощью этого оставленного объекта можно получить обратно результат выполнения запроса, когда тот будет готов.
СОВЕТ Интерфейс IAsyncResult далеко не нов, как и сопутствующие методы синхронизации. Вся эта механика должна быть хорошо знакома тем, кто сталкивался с асинхронными делегатами, асинхронной работой с файлами и другими многопоточными задачами в .Net. |
Одним из ключевых моментов при выполнении асинхронных операций является способ, с помощью которого инициатор операции может узнать о ее завершении. Для столь ответственного мероприятия ADO.Net предоставляет целых три способа:
Функция обратного вызова (callback). Все методы Begin* имеют перегруженный вариант, принимающий на вход делегат вместе с неким пользовательским объектом, в котором, в случае необходимости, можно передать текущее состояние. После того, как результат будет готов, произойдет вызов делегата. При этом необходимо учитывать, что делегат будет вызван из другого потока, поэтому могут потребоваться некоторые дополнительные действия для обеспечения синхронизации.
Объект синхронизации. Объект IAsyncResult, возвращаемый всеми методами Begin*, содержит свойство WaitHandle с событием, и это событие может быть использовано такими примитивами синхронизации, как WaitHandle.WaitAny и WaitHandle.WaitAll. Это позволяет вызывающему потоку дожидаться выполнения нескольких или всех запущенных операций, причем не только запросов к БД, но и, возможно, других асинхронных процедур или вызовов ОС, которые также обслуживаются вышеупомянутыми примитивами.