Посвященная этой функциональности глава в разделе BOL «новые возможности» называется Queue Processing Extensions - расширения обработки очередей. Но на самом деле, это всего лишь одно из самых очевидных применений данного механизма. Суть функциональности заключается в следующем: теперь у ряда операторов, занимающихся манипуляцией с данными, а именно INSERT, UPDATE и DELETE, появилось новое ключевое слово OUTPUT. С помощью этой конструкции можно после выполнения оператора получить результат его работы и перенаправить этот результат в какую-нибудь таблицу или просто вернуть клиентскому приложению. Если говорить проще, появился доступ к триггерным псевдотабличкам inserted и deleted прямо из запроса. Иными словами, теперь есть возможность узнать, что же именно было изменено DML-оператором, не обращаясь лишний раз к серверу.
Основное предназначение данной конструкции, как следует из названия раздела, это работа с очередями, но подробнее об этом будет сказано чуть позже, а пока разберем непосредственно механику.
Простейший пример может выглядеть примерно так:
-- создаем тестовую таблицу: -- CREATE TABLE OutputTest ( ID int IDENTITY, [Time] datetime default getDate(), Limit as Left(Data, 8), Data char(50)) -- собственно, проверяем, как оно работает: -- INSERT INTO OutputTest (Data) OUTPUT INSERTED.* VALUES (NewID()) -- наслаждаемся результатом: -- ID Time Limit Data 1 2005-05-21 19:40:43.087 5C1D39E9 5C1D39E9-8E28-4ED7-B5E8-938EA84FFE18 |
Как легко заметить, вся магия заключается в конструкции OUTPUT INSERTED.*, обратите внимание, что в тестовой таблице присутствует колонка identity, колонка со значением по умолчанию и колонка с вычисляемым значением. При этом данные, полученные из inserted-таблички, содержат уже посчитанные значения в этих колонках. То есть табличка inserted содержит фактические значения вставляемых данных уже после внутренних вычислений, однако триггеры не учитываются, то есть отработка output происходит после внутренних вычислений, но перед выполнением триггеров. Например, при наличии триггера INSTEAD OF на таблице, изменяющая эту таблицу операция в output вернет все данные, которые должны там быть, даже если в результате работы триггера никаких изменений не произойдет.
|
ПРЕДУПРЕЖДЕНИЕ На самом деле тут есть одно исключение, если на табличке висит триггер INSTEAD OF, то значение IDENTITY в OUTPUT INSERTED вычислено не будет. |
К выборке output можно применять различные выражения и подзапросы, в том случае если они возвращают одно значение. Например, если есть необходимость в момент изменения записи узнать, сколько времени прошло с момента последнего обновления, то запрос может выглядеть примерно так:
UPDATE OutputTest SET Data = newID(), [Time] = GetDate() OUTPUT DateDiff(ss, DELETED.[Time], INSERTED.[Time]) Diff |
В результате выполнения такого запроса получится рекордсет с одной записью, в которой будет содержаться количество секунд, прошедшее между изменениями записи.
В предыдущих примерах результат работы output отправлялся прямо в клиентское приложение, но можно перенаправить его и в таблицу – обычную, временную и табличную переменную. Сделать это довольно просто:
DECLARE @tmp_output TABLE ( ID_t int, Time_t datetime, Limit_t nvarchar(8), Data_t nvarchar(50)) INSERT INTO OutputTest (Data) OUTPUT inserted.* INTO @tmp_output VALUES (newid()) |
В данном случае вывод был перенаправлен в табличную переменную. В то же время, на таблицы, в которые производится вывод, наложено несколько ограничений:
На них не должно быть назначено триггеров. В принципе, триггер может быть назначен, но должен быть в состоянии Disabled.
|
Они не должны быть связаны внешним ключом с другими таблицами, и на эту таблицу не должны ссылаться внешние ключи.
Не должно быть CHECK-ограничений и правил (rules) в состоянии Enabled.
Вывод не может быть перенаправлен во view или функцию. Очевидно, это связано с запретом триггеров в целевой таблице.
Сложно сказать, с чем эти ограничения связаны, но, скорее всего, это вызвано стремлением максимально облегчить и ускорить вставку данных. Поскольку механизм output работает до триггеров, на довольно низком уровне, то желание избавиться от тяжелой функциональности вполне объяснимо.
И еще несколько общих ограничений механизма output:
Секционированные представления и удаленные таблицы не могут быть источником output.
В случае оператора INSERT источником output не могут быть view.
Порядок записей, выдаваемых output, не гарантируется.
Если вызов output происходит в триггере, и вывод из output не перенаправляется в таблицу, то, очевидно, опция disallow results from triggers не должна быть установлена, в противном случае произойдет исключение.
Также, если не происходит перенаправления вывода output, то изменяемая таблица не должна иметь активных триггеров на данную операцию модификации. Например, если происходит INSERT c output, без перенаправления вывода в таблицу, то триггеров на INSERT быть не должно, хотя UPDATE и DELETE триггеры вполне могут быть.
Как это использовать
Как ясно из названия данной функциональности, Microsoft предлагает использовать ее для работы с очередями. Во-первых, конструкцию DELETE … OUTPUT удобно применять для разгребания очереди, выполняя чтение и удаление прочитанной записи одним движением.
|
DELETE FROM output_test OUTPUT deleted.* |
В комбинации с хинтом READPAST, можно организовать разгребание очереди из нескольких потоков одновременно, если есть такая необходимость. Ранее для этого приходилось использовать как минимум два запроса с явным блокированием нужных записей.
Во-вторых, механизм output можно использовать и для формирования очередей. В данном случае это сильно напоминает работу обычного триггера, в задачу которого входит складывать измененные данные в отдельную таблицу. Но триггер будет срабатывать для всех изменений без исключения, а output можно использовать только в определенных DML-операторах. Иными словами, если таблица может изменяться из двух мест, то триггер будет срабатывать в обоих случаях, а output можно использовать только для одного из них. Да и использовать output, наверное, будет проще, чем триггер.
Применить данный механизм с пользой можно и в отрыве от очередей. Как уже упоминалось ранее, результат output возвращается после подсчета значений по умолчанию, identity и вычисляемых столбцов, и это может оказаться довольно полезным.
Однако основную проблему коммуникации между асинхронными процессами такой подход не решает – это всего лишь небольшой синтаксический сахарок, несколько облегчающий работу с собственноручно написанными очередями, но не более того. В идеале же механизм обмена, как уже говорилось, должен обеспечивать транзакционность, отсутствие дубликатов, автоматическую работу с очередями, обработку групп сообщений, гарантировать очередность и т.д… Все это богатство было реализовано и вошло в следующую версию SQL Server под именем Service Broker.
Service Broker
При первом взгляде на Service Broker возникает вопрос: "А что во что, собственно, встроено, СУБД в подсистему сообщений или наоборот.:)"
С одной стороны, полноценная подсистема работы с сообщениями должна иметь собственное надежное хранилище, без этого невозможно реализовать все богатство, описанное в предыдущем разделе. И ребята из Редмонда с присущим им размахом решили, что раз СУБД у них уже есть, то почему бы ей не послужить в роли хранилища данных для подсистемы рассылки сообщений? С другой же стороны, использовать SQL Server исключительно в роли хранилища для одной подсистемы, пусть и очень мощной, тоже как-то неправильно... Так и родилось то, что мы исследуем сейчас: без SQL Server-а работа подсистемы сообщений невозможна, но при наличии подсистемы сообщений SQL Server перестает быть просто хранилищем данных, как это было в недавнем прошлом.
Вышеупомянутая подсистема (под названием Service Broker) представляет собой очень мощный механизм, способный принести немало пользы. Описание этого сервиса может составить целую книгу. Кстати, вскоре одна такая выйдет, однако здесь будет описана лишь небольшая часть этой функциональности, необходимая для понимания дальнейшего материала.
СОВЕТ Книга, целиком посвященная Service Broker и выходящая в самое ближайшее время, называется The Rational Guide To SQL Server 2005 Service Broker Beta Preview (https://www.mannpublishing.com/Catalog/BookDetail.aspx?BookID=37). Она написана Роджером Уолтером (Roger Wolter), основная работа которого заключается как раз в руководстве группой, разрабатывающей этот самый Service Broker. Так что лучше него вряд ли кто-нибудь об этом механизме расскажет.:) |
Эта книга содержит много интересных и захватывающих подробностей, которые, к сожалению, выходят за рамки статьи, но рассмотрим вкратце, как все работает – процесс создания, посылки и получения сообщения.
Работа с Service Broker-ом реализована через набор объектов, которые управляются посредством обычных DDL операторов CREATE, ALTER, DROP - ничего нового. Команды по работе с этими объектами также являются небольшим DML расширением T-SQL. Например, команда получения сообщения возвращает обычный реляционный набор данных и мало чем отличается от SELECT, так что тут не должно быть никаких сложностей. В посылке и получении сообщения участвуют следующие объекты (здесь приведены далеко не все, лишь необходимый минимум):
QUEUE (очередь): Service Broker использует очереди для того, чтобы не было зависимости между отправителем и получателем сообщения. Отправитель просто помещает сообщение в очередь и идет заниматься своими делами, не дожидаясь получателя, а доставку сообщения возлагает на плечи собственно Service Broker-а, будучи уверенным, что тот справится. Получатель же может забрать и обработать сообщение, когда ему будет удобно, зная, что его деятельность никоим образом не влияет на эффективность работы отправителя, а все сообщения выстроены в должном порядке. При этом есть возможность запустить несколько «получателей» одновременно, добиваясь тем самым параллельной обработки очереди для достижения большей эффективности.
DIALOG (диалог): Одной из частей подсистемы доставки сообщений является диалог. Эту конструкцию можно рассматривать как двунаправленный поток (stream) между двумя конечными точками, по которому курсируют сообщения. Все сообщения в диалоге добираются до получателя ровно в том порядке, в котором были отосланы отправителем. Этот порядок сохраняется, невзирая ни на какие сбои, транзакции, архивирования базы, погодные условия, курс доллара и прочие внезапные факторы.
На самом деле, диалог является частным случаем общения (conversation). Conversation – это не объект, а более низкоуровневое понятие – постоянный, надежный канал связи. В MSSQL 2005 диалог – единственный тип общения, но в следующих версиях обещают добавить monolog (монолог), однонаправленный поток один-ко-многим, и, возможно, что-то еще. Однако в текущей версии диалог и общение можно считать синонимами.
Каждое сообщение включает в себя метку общения, которая уникально определяет диалог (а в будущем и другие типы потоков), ассоциированный с этим сообщением, что позволяет легко определить, откуда пришло сообщение, если поддерживается несколько каналов связи.
MESSAGE TYPE (тип сообщения): Любое сообщение должно быть ассоциировано с определенным типом. Это метка, которая передается вместе с сообщением и позволяет получателю понять, какого типа сообщение к нему приехало. По желанию, если сообщение представляет собой XML, то метка может быть ассоциирована с произвольной XML-схемой. В этом случае при получении производится проверка соответствия сообщения этой схеме, и если сообщение проверку не проходит, то оно отвергается.
CONTRACT (контракт): Контракт – это набор типов сообщений, при этом для каждого типа сообщения в контракте указывается, кому из участников диалога сообщение данного типа может принадлежать: отправителю, получателю или им обоим. Каждый диалог обязательно ассоциируется с контрактом. Иными словами, если тип сообщения не соответствует ни одному типу из контракта, ассоциированного с диалогом, то передать такое сообщение по этому диалогу не получится.
SERVICE (сервис): Сервис связывает несколько контрактов с очередью. Имя сервиса является синонимом для конечной точки диалога. Таким образом, контракт определяет, сообщения каких типов могут быть посланы через очередь посредством диалога, а сервис является конечной точкой, через которую сообщение попадает в очередь.
Схематично, весь этот зоопарк можно изобразить примерно следующим образом:
Отправитель и получатель – достаточно условные понятия, они имеют смысл только на этапе создания канала общения. После того, как канал создан, сообщения могут свободно гулять в обоих направлениях. Иными словами, отправитель – это та сторона, которая создает диалог. При этом само создание диалога не вызывает создания физического канала связи, канал создается лишь в тот момент, когда необходимо доставить первое сообщение, и удерживается до тех пор, пока диалог не завершится.
Через сервис отправителя сообщение попадает в очередь отправителя, если соответствует одному из типов сообщения, упомянутых в контракте. После этого оно передается в очередь получателя, где при получении также проходит проверку, после чего забирается оттуда собственно получателем. Если же получатель желает как-то ответить, то он в свою очередь формирует сообщение и отправляет его через свой сервис, и оно путешествует обратно тем же манером.
Теперь самое время приступить к практическим экспериментам. Для начала создадим все необходимые объекты:
-- тип сообщения, просто текст, для простоты безо всяких проверок и xml -- CREATE MESSAGE TYPE [TestType] VALIDATION = NONE -- теперь можно создать контракт, разрешающий сообщения этого типа -- для любой из сторон -- CREATE CONTRACT [TestContract] ([TestType] SENT BY ANY) -- для отправляющей стороны необходимо создать очередь -- и сервис на основе этой очереди -- CREATE QUEUE [SourceQueue] CREATE SERVICE [SourceService] ON QUEUE [SourceQueue] -- Для принимающей стороны так же нужно создать принимающую очередь -- и принимающий сервис, причем принимающий сервис обязательно -- должен иметь контракт, хотя для отправляющего это не обязательно -- CREATE QUEUE [TargetQueue] CREATE SERVICE [TargetService] ON QUEUE [TargetQueue] ([TestContract]) |
Все, объекты готовы. Теперь можно приступать собственно к передаче сообщения. В данном примере рассматривается самый простой вариант: из отправляющего сервиса сообщение попадает в очередь получателя и забирается оттуда.
Сначала займемся получателем. Для получения сообщения служит команда RECEIVE, которая сильно напоминает обычный SELECT, только вместо имени таблицы указывается имя очереди. К слову, и команда SELECT для очереди работает (поскольку с точки зрения базы данных очередь – это обычная таблица), показывая ее содержимое, но ничего из нее не удаляя. Команда же RECEIVE выбирает данные из очереди, удаляя выбранные сообщения. Однако если очередь пуста, RECEIVE отработает вхолостую и вернет пустой набор данных, а хотелось бы, чтобы кто-то караулил очередь, и RECEIVE бы срабатывала, как только в очереди что-то появится. К счастью, в этом нет ничего сложного, достаточно обернуть RECEIVE в WAITFOR. Итак, в отдельном окне выполняем следующую команду для своевременного получения сообщения:
WAITFOR(RECEIVE cast(message_body as nvarchar(MAX)) FROM [TargetQueue]) |
После выполнения этой команды подключение замрет в ожидании сообщения из очереди. Теперь самое время заняться отправителем. У него задачка посложнее, надо начать диалог и передать сообщение с идентификатором открытого диалога.
DECLARE @convHandler uniqueidentifier -- начало диалога -- BEGIN DIALOG @convHandler FROM SERVICE [SourceService] TO SERVICE 'TargetService' ON CONTRACT [TestContract]; -- посылка сообщения -- SEND ON CONVERSATION @convHandler MESSAGE TYPE [TestType] (N'Message!!!') -- завершение диалога -- END CONVERSATION @convHandler |
Если после отправки сообщения вернуться в окошко, где ожидали его получения, можно увидеть, что сообщение успешно получено.
Стоит заметить, что TargetService при создании диалога взят в кавычки, а SourceService – нет. Дело в том, что TargetService может быть создан на совершенно другом сервере, и просто отсутствовать на сервере, где начинается диалог такого сервиса.
Как можно видеть из примера, в каком именно диалоге отправлять сообщение, определяется некой меткой (handler), которая возвращается при создании диалога, и представляет собой GUID. Если ее в какой-то момент потерять, то завершить диалог можно будет только административными методами, узнав этот GUID из служебных представлений (catalog view). Эта же метка приезжает к получателю вместе с сообщением, и выбрав эту метку из очереди, можно отправить сообщение обратно в том же диалоге.
Асинхронные триггеры
Теперь рассмотрим, как можно использовать коммуникативные возможности Service Broker на сервере. Например, можно использовать его для реализации асинхронных триггеров, причем не только для DML- и DDL-операций, но и для событий, отслеживаемых профайлером (trace events), и если DML-триггеры придется реализовывать отчасти с применением обычных, то для DDL-триггеров и событий профайлера предусмотрен специальный механизм.
Асинхронные DML-триггеры
Начнем с 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 транзакции сообщение отправлено не будет. |
Как не сложно догадаться, этот механизм отслеживает события, на которые есть подписчики, и посылает соответствующее сообщение. Для того чтобы механизм сообщений заработал, достаточно создать очередь и сервис получателя с предопределенным контрактом [https://schemas.microsoft.com/SQL/Notifications/PostEventNotification], все остальное - и контракт, и диалог, и сервис с очередью отправителя, уже реализовано. Затем надо создать объект EventNotification, связывающий нужное событие с сервисом – и готово. На практике, допустим, для асинхронного аудита подключений к серверу и отключений от оного, это может выглядеть следующим образом:
-- сначала создадим очередь получателя, при желании -- здесь можно назначить процедуру обработки новых сообщений -- CREATE QUEUE [LoginQueue] GO -- затем необходимо создать сервис со специальным контрактом, -- в котором уже есть необходимые типы сообщений -- CREATE SERVICE [LoginService] ON QUEUE [LoginQueue]( [https://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. Это позволяет вызывающему потоку дожидаться выполнения нескольких или всех запущенных операций, причем не только запросов к БД, но и, возможно, других асинхронных процедур или вызовов ОС, которые также обслуживаются вышеупомянутыми примитивами.
Опрос (Polling). Объект IAsyncResult, помимо других полезных качеств, обладает свойством IsComplete, которое возвращает true или false в зависимости от того, завершена ли асинхронная операция. Соответственно, клиентский поток, занимаясь своими делами, может периодически опрашивать это свойство, и при получении положительного ответа, идти на поклон к методу End* за вожделенным результатом.
Можно также без излишних заморочек вызвать соответствующий метод End*, что блокирует вызывающий поток до получения результата, как в старом добром, абсолютно синхронном варианте. В глубине души-то он по-прежнему асинхронный, но из вызывающего потока это совершенно не будет ощущаться.
ПРИМЕЧАНИЕ Для выполнения асинхронных операций в строке подключения должна присутствовать ключевая фраза Asynchronous Processing = true (или async=true). В противном случае при попытке выполнить асинхронную операцию будет сгенерировано исключение. Однако если выполнение асинхронных операций не предполагается, то эту опцию рекомендуется попусту не использовать, так как это вызывает довольно заметный расход ресурсов на подключение, вплоть до того, что если в приложении предполагается активно использовать как синхронные, так и асинхронные запросы, то рекомендуется использовать две разные строки подключения, для синхронных и асинхронных запросов, соответственно. |
Вышеописанную функциональность можно использовать, например, при выполнении одновременно двух запросов к БД и последующей обработке их результатов на клиенте, что будет особенно эффективно, если базы физически находятся на разных серверах. Кроме того, можно выполнять асинхронные запросы из разных частей одной ASP.Net-страницы, что позволяет обновлять их параллельно. Вообще технология ASP.Net - довольно благодатная почва для использования асинхронных запросов. Это серверный механизм, который частенько вынужден иметь дело с огромным количеством обращений клиентов. В таких условиях потоки – вещь жутко дефицитная, и было бы непозволительной роскошью разбрасываться ими для ожидания выполнения запросов к базе. Новая функциональность здесь очень кстати, особенно в сочетании с асинхронными HttpHandler-ами.
Вот небольшой пример, позволяющий продемонстрировать некоторые возможности асинхронных запросов к серверу – примитивнейший индикатор выполнения длительного запроса, показывающий, что сервер чем-то занят, и не дающий пользователю впадать в панику.
Для начала – небольшая табличка, которая пригодится во всех последующих примерах:
CREATE TABLE AsyncTest( ID int IDENTITY, [Time] datetime default getDate(), Data char(50)) GO INSERT INTO AsyncTest(Data) VALUES (NewID()) INSERT INTO AsyncTest(Data) VALUES (NewID()) INSERT INTO AsyncTest(Data) VALUES (NewID()) |
А теперь собственно пример:
using System; using System.Data; using System.Data.SqlClient; namespace Rsdn.AsyncDemo { class AsyncTest { public void GetData() { using (SqlConnection connection = new SqlConnection( "Data Source=localhost\\ctpapril;Initial Catalog = cavy;" + "Integrated Security=SSPI;" + "Asynchronous Processing=true;")) { SqlCommand cmd = new SqlCommand( "WAITFOR DELAY '00:00:10' SELECT ID, [Time]," + "Data FROM dbo.AsyncTest"," connection); connection.Open(); // отсылаем асинхронный запрос на выполнение // IAsyncResult result = cmd.BeginExecuteReader(); // основной поток работает в цикле, каждую секунду проверяя, // не готов ли результат и выводя очередную точку в индикатор // while (!result.IsCompleted) { Console.Write("."); System.Threading.Thread.Sleep(1000); } // получаем готовый результат для отображения // SqlDataReader rdr = cmd.EndExecuteReader(result); while (rdr.Read()) Console.WriteLine(Environment.NewLine + rdr[0] + "\t" + rdr[2] + "\t" + rdr[1]); } } } } |
Как можно видеть, пример не слишком сложный. Из ключевых особенностей можно отметить
Asynchronous Processing = true |
в строке подключения. Эта строка обеспечивает работу с базой данных в асинхронном режиме. Еще один момент - WAITFOR DELAY в запросе, для имитации длительности его выполнения, дальнейшее должно быть очевидно из комментариев.
Несколько слов о некоторых особенностях данного механизма:
Как уже было замечено ранее, для использования асинхронного режима в строке подключения надо указать Asynchronous Processing = true (или просто async = true), но, как уже было замечено выше, пользоваться этой возможностью надо без фанатизма.
Каждому вызову Begin* обязательно должен соответствовать вызов метода End*, небрежность может привести к утечке ресурсов.
Если на сервер будет передан ошибочный запрос, который распознается как ошибочный до начала выполнения, то исключение будет выброшено методом Begin*, в противном случае, запрос считается выполненным, и исключение выбрасывается при вызове метода End*. К этому надо быть готовым.
В текущей бета-версии метод SqlCommand.Cancel() в асинхронном режиме не поддерживается, и неизвестно, будет ли поддерживаться в релизе.
Извещение об изменениях в результатах запроса (Query Notification)
Довольно часто возникает желание уведомить клиентское приложение о том, что в базе произошли некие изменения. На самом деле такое желание возникает гораздо чаще, чем встречается реальная необходимость в подобной функциональности. Но, тем не менее, бывают случаи, когда это действительно нужно. Поэтому в MS SQL Server 2005 и ADO.Net 2.0 была реализована поддержка подобного сценария.
Со стороны SQL Server в этом предприятии участвуют собственно ядро сервера (Sql Engine), Service Broker и специальная хранимая процедура sp_DispatcherProc. Со стороны ADO.Net участвуют классы SqlNotificationRequest и SqlDependency из пространства имен System.Data.SqlClient. Кеш ASP.Net также поддерживает эту функциональность. Для этого используется класс System.Web.Caching.Cache.
В общем виде сценарий использования уведомлений об изменении запрошенных данных выглядит примерно так:
У объекта SqlCommand, который содержит запрос, в процессе его инициализации заполняется свойство Notification, которое содержит подписку на оповещение об изменениях запрошенного набора данных (это свойство (Notification) передается на сервер вместе с запросом).
После получения пакета с таким свойством сервер регистрирует подписку на изменения и выполняет пришедший запрос в обычном порядке, отсылая результат клиенту.
Ядро сервера следит за всеми DML-операциями, которые могут привести к изменению результата запроса, и если сервер подозревает, что результат был изменен, ServiceBroker-у посылается специальное сообщение об этом.
Далее, в зависимости от применяемого сценария, сообщение может быть сразу отправлено клиентскому приложению или же помещено в очередь, чтобы клиентское приложение забрало его само.