Настоящая статья основывается на материалах докладов, сделанных автором на ежегодной конференции разработчиков DevCon. Учитывая повышенный интерес, проявленный аудиторией к данной тематике, а также возрастающую популярность Microsoft Transaction Server среди разработчиков многоуровневых систем несмотря на сравнительную "молодость" продукта, было бы целесообразно, на мой взгляд, немного более подробно остановиться на назначении и функциональных возможностях Microsoft Transaction Server, опуская, по мере возможности, специальные технические вопросы, затронутые в докладах, так как представляется гораздо более важным дать читателю общее видение продукта и его место в современной компонентной модели построения программного обеспечения, нежели дублировать документацию. Пользуясь случаем, я хотел бы поблагодарить многочисленных участников конференции DevCon'97, чьи вопросы во многом определили содержание данной статьи, и главного редактора издания "Технология клиент-сервер" В.Ю.Чистякова за ценное обсуждение ряда аспектов использования Microsoft Transaction Server и предоставленную возможность рассказа о них.
Идея повторного использования кода не является чем-то принципиально новым в мире разработки программного обеспечения. Как известно, лень - двигатель прогресса (J). Если смотреть на вещи с несколько философской точки зрения, то все развитие человеческой цивилизации обусловлено возможностью основываться на результатах труда предшествующих поколений, расширяя и совершенствуя эту базу, в свою очередь, для наших потомков. Нет ничего удивительного поэтому, что попытки внедрения подобного подхода в области программных технологий предпринимались, наверное, еще на заре развития программирования. (Мы заведомо абстрагируемся от тех вырожденных случаев, когда условия оплаты заведомо подталкивали программистов к изобретению велосипеда и проч.) Сначала это, вероятно, был примитивный перенос кусков кода, который упростила директива #include
, затем с появлением процедурных языков это стали библиотеки процедур, наконец, по мере того, как объектная парадигма овладела умами и чаяниями разработчиков средств разработки, их место в значительной степени заняли классы и библиотеки классов. Собственно говоря, перенесение основных свойств объектов окружающего нас мира в ту его прекрасную часть, которая относится к разработке софта,- возьмем хотя бы наследование- уже было подчинено идее переиспользуемости. Однако ни процедуры, ни классы не позволяли в полной мере воспользоваться преимуществами наследования от уже разработанных независимых модулей при создании достаточно сложного проекта, в особенности, если его предполагалось растиражировать для большого числа заказчиков. Основным препятствием была практическая невозможность установить новую версию библиотеки без ущерба для приложений, написанным с учетом предыдущих версий API. В общем случае это влекло за собой повторную компоновку, а то и компиляцию проекта, что вынуждало поставщика программного решения передавать клиенту не только исполняемые или (в крайнем случае) объектные модули, но и исходные тексты программ. Во-первых, очевидно, что это отнюдь не способствовало охране интеллектуальной собственности авторов, а во-вторых, даже эти жертвы становились напрасны, если клиент вдруг решал использовать средства разработки иного производителя. Благим намерениям создателей С как некоего не зависящего от платформы стандарта языка программирования не суждено было сбыться, и со временем рынок ПО захлестнула волна компиляторов С/С++, для каждого из которых, как водится, немедленно возникли толпы поклонников, готовых истово отстаивать, что их любимый компилятор именно этой фирмы является самым компилятором в мире. На сегодня это число заметно уменьшилось, и страсти сами собой улеглись- речь не об этом. К несчастью, С++ не является единственным объектно-ориентированным языком, и если не столь долгое время назад заказчик желал, чтобы приобретенные у вас компоненты, написанные на том же С++, интегрировались в среду его "домашних" разработок, скажем, на Pascal'e, то это была далеко не тривиальная задача, где не спасали ни исходники, ни их косметическая правка. Таковы примерно были факторы, которые в конце концов привели к идее создания новой технологии и выработке стандартов, позволяющих избежать зависимости от конкретного средства разработки компонент и проводить эффективное обновление версий без негативных последствий для связанных с ними модулей. Реализация этой технологии, выполненная корпорацией Microsoft, получила название модели компонентных объектов (COM).
Художник не рисует картину по пикселам. Сначала на холст ложатся грубые штрихи, и только потом наступает прорисовка деталей. Идея компонентного подхода в программировании возникла одновременно с программированием, поскольку она соответствует, если можно так выразиться, естественной модульности аналитического мышления, когда решение достаточно объемной задачи начинается с ее разбиения на несколько подзадач, те, в свою очередь, дробятся на еще более мелкие и иерархически более зависимые фрагменты и т.д. вплоть до тех деталей, решение которых элементарно. По той же причине компоненты упрощают отладку приложения- вспомните поиск корня методом половинного деления. Более того, сборка из независимых компонент существенно увеличивает масштабируемость, так как в зависимости от требуемой функциональности мы можем добавить в него те или иные компоненты или, наоборот, изъять так, чтобы приложение не обрушивалось, а плавно и корректно деградировало. Мы можем физически "размазать" приложение по сети, установив разные компоненты на разные компьютеры, и снова повысить тем самым масштабируемость, но уже в плане производительности. Наконец, компоненты универсальны, как атомы или гены: компоненты, из которых состоит ваше приложение, в различных сочетаниях с другими компонентами могут использоваться при построении новых приложений, т.е. именно благодаря им суждено сбыться вековой мечте человечества о повторной используемости кода, с которой мы начали наш рассказ. Итак, компоненты- это круто. Теперь несколько слов о том, как это все работает.
Распределенная компонентная модель объектов (DCOM)- это (см.[1]) набор стандартов построения, размещения и взаимодействия компонент и реализующих их механизмов, которые позволяют объектам внутри компонент посылать вызовы другим компонентам, а также принимать и обрабатывать запросы от других компонент вне зависимости от их положения на компьютере или в сети, от способов реализации, от того, являются ли они прикладными или объектами операционной системы и т.д. Для этого объекты (D)COM "договариваются" о предоставлении друг другу сервисов через строго определенные интерфейсы, которые на идеологическом уровне можно рассматривать как своего рода обязательство объекта предоставить заявленную функциональность при условии вызова в соответствии с опубликованными им правилами, а на бытовом- как группы семантически связанных функций, объединенных в абстрактные виртуальные классы. Пусть, например, имеем некоторый набор функций, оформленный в виде
struct I1 { virtual void f11()=0; virtual int& f12()=0; ... }
Любой класс, намеренный использовать эту функциональность, может быть оформлен, например, в виде наследования:
class AnyCls : public I1 ...
Однако, что предпринять, если нам потребовалось расширить набор функций? Переписать struct I1
означает вернуться к проблемам, описанным в п.1. Следовательно, логично будет свести дополнительные функции в новый интерфейс:
struct I2 { virtual void f21()=0; ... }
А как быть тогда с AnyCls? Интерфейсов может не один и не два, а множественное наследование выручает до известного предела, после которого возникают разного рода проблемы, начиная от слабой читаемости программы и заканчивая потерями в производительности. Решение состояло в использовании вложенных классов:
class AnyCls { protected: ... public: AnyCls {...;} class C1 : public I1 { public: C1() {} virtual void f11(); virtual int& f12(); ... } m1; class C2 : public I2 { public: C2() {} virtual void f21(); ... } m2; ... } void AnyCls::C1::f11(){ ... ;} ..., которые, с одной стороны, упакованы в родительском классе и не видны за его пределами, а с другой, имеют доступ к его элементам. Заметим, что с помощью функций CoCreateInstance или IUnknown::QueryInterface (см.ниже) клиент работает только с указателями на таблицы виртуальных функций интерфейсов, т.е. реально создав объект того же класса AnyCls, он тем не менее не имеет указателя на него. Таким образом он никогда не получает прямого доступа к внутренним данным объекта, даже если бы они не были protected. Моделированию на С++ основных механизмов работы COM посвящена, например, [2], часть IV и желающие всегда могут обратиться к этой литературе, а также к [3], которая по справедливости считается основополагающей работой в области компонентного подхода, до сегодняшнего дня не утратившей своей актуальности.
Интерфейсы являются настолько основополагающим понятием COM, что для их описания используется специальный язык- IDL (язык определения интерфейсов), по своей структуре очень похожий на С++. В определении интерфейса описываются заголовки входящих в него функций с указанием их имен, возвращаемых типов, входных и выходных параметров, а также их типов. Это и есть та часть контракта, которая будет сообщать, что может и как надо вызывать объект, взявший на вооружение данный интерфейс. Каждый интерфейс получает свой 16-байтовый глобальный идентификатор, который присваивается ему программой генерации на основании времени создания, адреса сетевой платы и пр. После опубликования интерфейс фиксируется и дальнейшие изменения в нем не допускаются. IDL допускает наследование, так что возможны производные интерфейсы. Более того, все интерфейсы являются производными от одного базового интерфейса под названием IUnknown. В состав IUnknown входят 3 метода. QueryInterface используется для получения указателей на другие интерфейсы объекта, при условии, что указатель на начальный интерфейс был получен при помощи CoCreateInstance. Методы AddRef и Release применяются для подсчета ссылок, т.е., грубо говоря, сколько клиентов в данный момент используют интерфейсы данного объекта. AddRef выполняется автоматически, как только со стороны клиента поступает запрос на указатель интерфейса. Однако если внутри клиентской программы происходит порождение новой ссылки на интерфейс, о которой серверный объект не догадывается, вызов AddRef возлагается на клиента. Клиент не имеет права разрушить серверный объект, но по окончании работы с ним он обязан вызвать метод Release, который уменьшит на единичку число пользователей объекта. Когда оно станет равным 0, объект сам себя уничтожит.
В том случае, если объект создан как in-process сервер (dll), т.е. выполняется внутри клиентского процесса, особых проблем не возникает. Если же объект реализован в виде out-of-process сервера, выполняющегося как отдельный процесс на том же или удаленном хосте, немедленно появляется вопрос о передаче указателя на интерфейсы и параметров вызовов методов между процессами. Вопрос нетривиальный тем более потому, что различные компьютеры могут использовать разные форматы представления данных. Ответ состоит в использовании proxy-объекта внутри клиентского процесса и заглушки внутри сервера. Proxy- это обычный COM-объект, который представляет те же интерфейсы, что и вызываемый клиентом, однако вместо непосредственного вызова методов он принимает переданные клиентом параметры и упаковывает их для передачи средствами межпроцессной или межмашинной (RPC- remote procedure call) коммуникации. Заглушка на стороне сервера принимает и распаковывает эти параметры, вызывает соответствующий метод серверного объекта и передает их ему. Возвращение результатов клиенту происходит по той же схеме в обратном направлении. Такой процесс называется маршалингом (демаршалингом). Обычно все развитые средства разработки COM-объектов предоставляют возможность автоматической генерации proxy и заглушки для интерфейса. Если разработчика по каким-то причинам это не устраивает, он может запрограммировать нестандартный маршалинг при помощи определенного в COM интерфейса IMarshal. Возникает интересный момент: если код маршалинга способен автоматически создаваться на стадии компиляции программы, то почему бы не использовать эту возможность для динамической генерации маршалеров во время выполнения программы? Такой подход (он известен как позднее связывание), несмотря на дополнительные затраты, существенно повышает гибкость программирования, ибо решение о вызовах тех или иных интерфейсов может приниматься по ходу дела в зависимости от прикладной логики. Однако для этого клиент должен "на лету" уметь получать доступ к информации об интерфейсах, необходимой для выполнения маршалинга. Этим требованиям отвечает библиотека типов, которая перечисляет все интерфейсы, поддерживаемые тем объектом, для которого она создается, и описывается в терминах уже упоминавшегося нами выше IDL. Как и для COM-объектов, при установке в registry записывается ID библиотеки и ее местоположение. При вызове средствами СОМ API библиотека типов предоставляет интерфейс ITypeLib, который, в свою очередь, позволяет получить указатели на интерфейс ITypeInfo для каждого интерфейса, перечисленного в библиотеке, с помощью которых добывается информация о параметрах методов и их типов, необходимая для динамического маршалинга.
Обычно основным потребителем этой информации оказывается интерфейс IDispatch (естественно, производный от IUnknown), который наверняка знаком программистам на Visual Basic. Методы этого интерфейса GetTypeInfo и GetTypeInfoCount позволяют динамически запрашивать запущенный объект относительно информации о всех его интерфейсах. Другой метод, IDispatch::Invoke фактически представляет собой оператор switch, который в зависимости от переданного значения идентификатора (DISPID) вызывает тот или иной метод диспинтерфейса. Иногда в литературе можно встретить суждение, что Visual Basic не работает с указателями, поэтому для него потребовалось создать некий универсальный механизм, умеющий работать только с одним интерфейсом IDispatch. Это не совсем корректное замечание. Во-первых, в свете нашего разговора о маршалинге очевидно, что единственная пара proxy-заглушка для IDispatch не в состоянии предусмотреть преобразование параметров для самых различных методов диспинтерфейса, поэтому часть преобразования неявно выполняет сам клиент, упаковывая параметры в variant. Во-вторых, только первая версия Visual Basic распознавала исключительно диспинтерфейсы. Во всех остальных случях QueryInterface совершенно аналогично возвращает указатель на виртуальную таблицу, содержащую реализацию запрашиваемого интерфейса. В терминах Visual Basic это известно как объектная ссылка (object reference). Т.е., если переменная объявлена как dim x As <Something>
, где <Something>№ Object
, то будет использоваться раннее связывание. В первую очередь компилятор (VB5) будет искать способы прямого вызова через виртуальную таблицу (vtable binding) и только в случае неудачи прибегнет к диспинтерфейсу. При этом по информации в библиотеке типов он постарается найти DISPID и кэшировать его, чтобы сэкономить на достаточно дорогом вызове GetIDsOfNames в период выполнения. При ссылках типа dim x As Object
или Set x = y
, где создание объекта y
происходит во время работы программы, компилятор, естественно, лишен возможности сделать какие-либо предположения о природе объектов х
и не сможет оптимизировать их вызовы. В этом случае, как нетрудно догадаться, будет иметь место позднее связывание. Позднее связывание преимущественно характерно для Visual FoxPro 5.0, в него также включена оптимизация для поддержки vtable binding- см. функцию sys(2333). Иногда интерфейсы описываются как двойственные, т.е. содержащие диспинтерфейсные представления для виртуальных таблиц своих методов.
Одним из ярких проявлений преимуществ компонентной модели служит OLE-автоматизация, или программируемость, тесно связанная с двойственными и диспинтерфейсами. OLE-автоматизация явилась одним из этапов развития COM, и практически каждый наверняка с ней сталкивался. Речь идет о том, что при всей полноте своей функциональности приложение будет представлять еще большую ценность для разработчиков, если позволит использовать эту функциональность не только в интерактивном режиме, но и из пользовательских программ. Вместо того, чтобы снабжать приложение дополнительным API для этих целей, автор может просто оформить его как OLE Automation сервер, что приблизительно означает, что приложение сознательно "засвечивает" вовне некоторые из своих методов, использовавшихся для внутренней реализации заявленной функциональности. При этом, во-первых, достигается экономия программистского труда: автору не нужно сначала разрабатывать приложение, а потом писать API к нему. Во-вторых, пользователь мыслит в тех же категориях, что и автор, что значительно облегчает освоение программы. В третьих- и это плюс, характерный для всей технологии COM,- предоставленная функциональность остается на вооружении пользователя при разработке им каких-то своих программ, никак не связанных с данным OLE Automation сервером. Простой пример: если у нас на компьютере установлен Microsoft Office, то зачем нам изобретать велосипед и писать программу проверки орфографии, если мы можем создать объект Word.Application и вызвать для него метод SpellChecking:
dim x As New Word.Application Debug.Print x.SpellChecking("Abra Cadabra") ...
То же самое относится, например, к использованию статистических функций Microsoft Excel и т.д. В качестве OLE Automation серверов могут рассматриваться не только офисные приложения, но и сами средства разработки- тот же Visual Basic или Visual FoxPro- и даже тяжелые серверные продукты семейства Microsoft BackOffice, например, Microsoft SQL Server, который с помощью SQL-DMO (distributed management objects) обеспечивает выполнение практически всех административных функций из клиентского приложения (разумеется, при наличии соответствующих прав доступа).
dim oSQLServer As New SQLOLE.SQLServer oSQLServer.Connect "ntalexejs", "sa" dim newdb As New SQLOLE.Database newdb.Name = "sqlole" newdb.ExtendOnDevices ("oledat=5") newdb.TransactionLog.DedicateLogDevices ("olelog=2") oSQLServer.Databases.Add newdb
Примерно в это же время, а может быть, несколько раньше, разработчики СУБД столкнулись с необходимостью поддержки неструктурированных типов данных: мультимедиа, геопространственная информация, папки электронной почты и т.д. С идейной точки зрения, не принимая в расчет вопросов технической реализации, решение, казалось бы, лежит на поверхности: переписать заново или переработать ядро своего сервера баз данных, сделав его объектно-реляционным и включив туда поддержку новых типов наравне с традиционной информацией. Выигрыш очевиден: мы снова получаем единый API и полный контроль над данными и их изменениями. К сожалению, минусов при таком подходе оказывается больше, чем плюсов. Во-первых, требуется перекачка информации из исходных мест ее хранения в базу данных, где она будет обрабатываться, преобразовываться и возвращаться обратно. Естественно, это не лучшим образом сказывается на производительности и надежности. Во-вторых, выполнение запросов очень трудно поддается оптимизации, так как правила их обработки сильно зависят от типов данных. В-третьих, схема носит закрытый характер и оттого не является привлекательной в глазах разработчиков независимых приложений. Наконец, не вполне понятно, что делать, когда возникнет нужда в поддержке новых типов данных,- снова переписывать ядро? Поэтому следующим этапом стало проектирование универсальных серверов баз данных, где процедуры обработки неструктурированной информации обладали возможностью гибко встраиваться в технологию работы исполнительного механизма СУБД. В одном случае такие процедуры были оформлены как динамические библиотеки и работали в том же процессе, что и ядро универсального сервера, в другом- как самостоятельные процессы. При этом часть процедур отвечала за поддержку дополнительных типов данных, которые наряду с традиционными хранились в базе, а часть (во взаимодействии с сетевым сервером приложений) - за их обработку. В скором времени между приверженцами этих двух направлений разгорелась война не на шутку, которая вышла за пределы соревнования технологий и переместилась на придорожные рекламные щиты и в отделы по найму персонала. Первый подход критиковался с точки зрения надежности, так как чисто теоретически можно допустить, что случайная ошибка в процедуре способна вывести из строя весь процесс, т.е. ядро СУБД, тем более, что право на написание встраиваемых модулей было также даровано независимым фирмам. Второй подход в силу изоляции процессов друг от друга выглядел более надежным, но по той же причине подвергался критике с точки зрения производительности. В последнее время достаточной популярностью пользуется идея компонентного подхода, когда данные не переносятся в базу, а используются по месту их непосредственного хранения. Для каждого типа данных существует компонент, который мы назовем, скажем, провайдером OLE DB и который по своему принципу работы напоминает хорошо известные ODBC-драйверы, с той лишь разницей, что он умеет обращаться к данным не обязательно реляционной природы. Каждый такой компонент "знает", как обеспечить доступ к своему типу и представляет для этого необходимые (стандартные) интерфейсы. В результате способы представления информации оказываются прозрачными для потребителя, и он сможет в одном операторе select запрашивать, например, информацию из различных баз данных, электронных таблиц, документов, электронной почты, файловой системы и т.п.
Проникновение компонентных технологий в область баз данных было даже в какой-то степени обусловлено исторически. Перенесемся буквально на несколько лет назад, и мы попадем в эпоху расцвета персональных настольных СУБД a la xBase, где пользовательский интерфейс, средства программирования бизнес-логики, обеспечение целостности схемы хранения данных- все было заключено внутри одного продукта. За относительно недолгое время необходимость корпоративной работы над проектом, рост объемов хранимой и перерабатываемой информации и многое другое привели к внедрению клиент-серверных систем, где на сервер базы данных возлагалась ответственность за безопасность хранения и доступа к данным, поддержку их транзакционной целостности, обеспечение пользовательских соединений и многое другое. На клиенте, соответственно, остались пользовательский интерфейс и какая-то часть бизнес-логики. Вообще, бизнес-логика на сервере начинается с момента проектирования базы данных, и потому лично для меня вполне разумным представляется стремление сосредоточить как можно большую ее часть в триггерах, хранимых процедурах и т.п. Клиент-серверные системы получили широкое признание и распространение вплоть до уровня масштабных корпоративных и общегосударственных проектов. Невероятно разрослась и усложнилась бизнес-логика, она перестала умещаться внутри сервера базы данных. Плюс к тому продолжали оказывать влияние те самые тенденции, которые обусловили переход от персональных баз к клиент-серверным системам: увеличение объемов данных, транзакционной нагрузки, числа одновременных пользовательских коннектов и т.д. В итоге на клиенте остался интерфейс, на сервере баз данных- его типичные функции типа поддержки целостности схемы, резервное копирование, тиражирование, а все, что относилось к бизнес-логике, выделилось в самостоятельное промежуточное звено (middlware). Не успели утихнуть восторги по поводу того, как классно стало жить теперь нам всем, как выяснилось, что новое- это хорошо забытое старое, за все приходится платить, и вместе с бизнес-логикой на хрупкие плечи разработчиков среднего звена ложится обязанность программирования безопасности доступа, управления потоками, обработки транзакций, обеспечения необходимого быстродействия и масштабируемости и много чего еще, с чем раньше благополучно справлялся сервер баз данных. Более того, столь тесное переплетение системного и прикладного программирования не только удлинило сроки разработки и подняло примерно вполовину стоимость такого проекта, но, что еще более печально, увеличило зависимость результатов от конкретного проекта и сделало практически невозможным повторное использование наработок в этой области. В этом месте оставалось бы вздохнуть, развести руками и поставить точку, но я с детства не люблю сказок с несчастливым концом, поэтому в нашем повествовании появляется новое действующее лицо- Microsoft Transaction Server.
18 мая в рамках проходившего в Нью-Йорке Дня Масштабируемости демонстрировалась работа гипотетической банковской системы, клиентами которой являлась примерно четверть населения земного шара. Общая база данных находилась под управлением 20-ти серверов Microsoft SQL Server 6.5 на платформе Compaq. Еще 20 компьютеров имитировали деятельность со стороны клиентов. Диспетчеризацию клиентской нагрузки и управление транзакциями выполняли 5 серверов Microsoft Transaction Server (MTS). За день система смогла обслужить миллиард(!) транзакций, из которых значительная доля пришлась на долю распределенных (т.е. проходящих через несколько серверов баз данных).
Microsoft Transaction Server 1.0 был выпущен в декабре прошлого года и в традиционном понимании является сервером поддержки работы приложений, составляющих ПО промежуточного слоя. Он осуществляет автоматическое управление процессами и потоками, имеет встроенные службы безопасности для контроля вызовов и использования объектов, обеспечивает поддержку распределенных транзакций по протоколу двухфазной фиксации OLE 2PC и интеграцию с MS DTC, предоставляет графический интерфейс для регистрации и управления компонентами (MTS Explorer), т.е. фактически предоставляет готовые средства решения задач системного программирования, которые, как мы отметили выше, неизбежно возникают при разработке middleware. С этой стороны положительный аспект применения MTS заключается в том, что при разработке компонент не нужно программировать вручную реакцию на разнообразные исходы в системе. Воспользуемся одним из примеров в составе MTS и рассмотрим класс Account компоненты Bank. Он имеет метод Post для дебитования или кредитования определенного банковского счета. Однако, как правило, банковская операция означает дебет одного счета и кредит другого. Вопрос: сколько дополнительного программирования потребуется, чтобы вызов двух методов в программе на VB, VC++ и т.д. выполнялся как одна транзакция? С использованием MTS решение становится тривиальным.
dim ctxObject As ObjectContext Set ctxObject = GetObjectContext() ... On Error GoTo ErrorHandler dim objAccount As Bank.Account Set objAccount = ctxObject.CreateInstance("Bank.Account") ... objAccount.Post(lngSecondAccount, lngAmount) objAccount.Post(lngPrimeAccount, 0 - lngAmount) ... ctxObject.SetComplete ... ErrorHandler: ctxObject.SetAbort ...
Мы значительно сократили и упростили текст примера, оставив в нем иллюстрацию самых базовых моментов. Первое, на что нужно обратить внимание- это объект ctxObject, который носит название "контекст объекта". Каждый объект, размещенный в MTS, автоматически получает в соответствие такой объект на все время своей жизни. Контекст объекта управляет участием объекта в транзакциях под контролем MTS (см., например, методы SetComplete и SetAbort в примере) и предоставляет средства для программной проверки безопасности. Например, если поступает запрос на перевод крупной суммы, мы можем предусмотреть проверку на наличие у него соответствующих прав:
If (lngAmount > 500 Or lngAmount < -500) Then If Not ctxObject.IsCallerInRole("Managers") Then Err.Raise Number:=ERROR_NUMBER, ... Description:= "Need 'Managers' role for amounts over $500" End If End If
Установка самих ролей происходит в MTS Explorer. Ccылку на контекст объекта можно получить с помощью функции MTS API GetObjectContext(). В данном примере мы создаем объект Account от контекста объекта MoveMoney, в результате чего он будет выполняться внутри той же транзакции, что и объект MoveMoney. В общем случае это зависит от транзакционного атрибута компоненты, под которым она установлена в MTS. Возможны 4 варианта: supported- при создании нового объекта его контекст наследует клиентскую транзакцию, если же клиент исполняется вне транзакции, то новый контекст тоже не будет транзакционным; required- то же, что и в предыдущем случае, но в случае отстутствия транзакции у клиента MTS откроет новую транзакцию для объекта; requires new- объект не наследует транзакцию клиента, для него всегда открывается новая транзакция; not supported- транзакция никогда не будет открыта объекту вне зависимости от наличия транзакции на клиенте.
Одной из отличительных черт MTS является чрезвычайно рачительное, я бы даже сказал, трепетное отношение к системным ресурсам. Его девизом является активизация по мере необходимости и как можно более быстрая деактивизация объектов. Когда MTS деактивизирует объект, тот не разрушается, а становится доступен для повторного использования другими объектами. По секрету скажу, что MTS зачастую наглеет настолько, что может деактивизировать созданный вами объект при живых клиентских ссылках. То есть если вы создали объект, а потом отошли кофейку попить, не удивляйтесь, что этот сквалыга уже спер ваш объект, деактивизировал его и загнал кому-нибудь на сторону. Сердиться на него за это не стоит, потому что когда вы вернетесь и скажете "эй, парень, гони взад мой объект", лучше подождать лишних пол-секунды, пока MTS создаст или утянет (что существенно быстрее) с очередного ленивого соединения такой же объект, чем получить сообщение типа "извини, брат, не могу- память кончилась". MTS деактивизирует объект при вызовах методов окончания транзакции SetComplete или SetAbort. В следующий раз при повторном обращении к этому объекту MTS по обыкновению вмешается в COMовский вызов, подсунет вам тот самый ваш (а может и не ваш) объект и вызовет у него затребованный вами метод. То же происходит при работе с базами данных. Так как одной из самых дорогих операций является установка соединения, то MTS склонен держать у себя пул ODBC-соединений, и если нужное вам соединение уже туда попало и является свободным, как вы думаете, что сделает MTS, когда вы его попросите соединиться с базой? Кинется бегом открывать новое соединение? Щас прям. В подавляющем большинстве случаев вы получите его из пула. Смех смехом, однако бережное расходование системных ресурсов является одним из необходимых условий высокой масштабируемости, и не обладай MTS такими возможностями, он едва ли сумел бы на обычной в общем-то конфигурации без особых наворотов, обработать миллиард транзакций в сутки.
Иногда возникают ситуации, когда объект вынужден вернуть управление клиенту не будучи готовым завершить транзакцию. Пусть у нас есть объект, отвечающий за ввод одной бухгалтерской проводки. Нам нужно ввести серию проводок, скажем, за день и сверить сумму с итогом. Если они совпадают, то только тогда всю группу проводок можно фиксировать как единую транзакцию. Для этих целей контекст объекта имеет метод DisableCommit, который запрещает деактивизацию и сброс внутреннего состояния объекта. В нашем случае его должен вызвать объект ввода проводки. После выверки итогов объектом вызывается метод EnableCommit с последующей фиксацией или откатом транзакции.
Итак все объекты, работающие в среде управления MTS, могут вызывать друг друга. В отличие от них базовым клиентом называется истинный клиент в рассмотренной нами в п.3 трехуровневой схеме, т.е. это приложение, работающее, очевидно, не под управлением MTS, основная цель которого обеспечивать пользовательский интерфейс и отображать запросы пользователя в вызовы компонент. Для управления работой объектов MTS базовый клиент использует объект TransactionContext. Несмотря на то, что схема его применения очень похожа на контекст объекта (методы CreateInstance, Complete и Abort), базовый клиент может только управлять транзакцией MTS, но не участвовать в ней. Например, он не может напрямую открыть соединение с базой данных и вставить операции над ней внутрь этой транзакции. Наверное, это правильно, потому что коль скоро бизнес-логика ушла из клиентской части, то участие в транзакциях middleware недопустимо для базового клиента.
VB4 умел создавать только однопоточные компоненты. MTS обеспечивает их поддержку, хотя это самый медленный способ, чреватый взаимными блокировками. Предположим, объекты А и В последовательно исполняются на одном потоке управления. А блокирует запись Х и заканчивает работу, забыв сделать SetComplete. Управление получает объект В, которому тоже нужна запись Х, поэтому он простаивает в ожидании, пока А ее разблокирует. Но для этого А должен получить поток управления, который наглухо занял В. Имеем картину под названием "Приплыли". В VB5 можно создавать apartment-threaded компоненты, которые также поддерживает MTS, что уже легче. Каждому объекту внутри компоненты назначается отдельный поток на все время его жизни. Границы апартаментов определяются деятельностью (activity), которая, насколько я понимаю, означает "гирлянду" объектов, связанных последовательными вызовами. Таким образом, если два объекта принадлежат разным деятельностям, они могут выполняться параллельно независимо от того, принадлежат они одной или разным компонентам. Мне кажется, что логично было бы ввести в последующих версиях MTS модель рабочих потоков, так чтобы объекты не привязывались жестко к какому-то потоку, а могли свободно между ними перераспределяться и при этом не требовался бы кросс-поточный маршалинг.
Административными единицами при размещении компонент в MTS служат пакеты. Считается, что компоненты внутри пакета полностью доверяют друг другу, поэтому взаимные права компонент в пакете не проверяются, кроме того они запускаются в одном процессе. Очевидно, что базовые вызовы имеют identity клиента, поэтому при доступе к компонентам внутри пакета проверяется user-id. Вызовы, идущие из пакета, уже имеют identity данного пакета, поэтому если пользователи обращаются к компонентам, а компоненты, в свою очередь,- к базе данных, то имеет смысл сгруппировать компоненты по пакетам в соответствии с правами пользователей на доступ к базе, в противном случае авторизацию придется прописывать ручками внутри компонент. Это своего рода издержки переходного периода к многоуровневым системам. Перестройте свое мышление в соответствии с новым подходом: вы уже выросли из системы "клиент-сервер", давайте права на базу, имея в виду не конечных пользователей, а компоненты. В конце концов, пользователь вообще теперь не работает с базой- это забота компонент. Администрирование пользовательского доступа к компонентам осуществляется из MTS Explorer.
MTS допускает введение внутри пакетов глобальных переменных, доступ к которым разрешен для каждой компоненты. Глобальные переменные (свойства) организованы в группы, получить или создать которые можно с помощью SharedPropertyGroupManager. Аналогично внутри каждой группы можно получить или создать разделяемое свойство при помощи SharedPropertyGroup. Вернемся к нашему банковскому примеру в начале пункта. Предположим, что мы еще хотим протоколировать каждую проводку в журнале и присваивать ей согласованный уникальный номер. Вот как для этого могут использоваться разделяемые свойства:
dim spmMgr As SharedPropertyGroupManager Set spmMgr = CreateObject("MTxSpm.SharedPropertyGroupManager.1") dim spmGroup As SharedPropertyGroup dim bResult As Boolean Set spmGroup = spmMgr.CreatePropertyGroup("Receipt", LockSetGet, _ Process, bResult) dim spmPropNextReceipt As SharedProperty Set spmPropNextReceipt = spmGroup.CreateProperty("Next", bResult) dim spmPropMaxNum As SharedProperty Set spmPropMaxNum = spmGroup.CreateProperty("MaxNum", bResult) dim objReceiptUpdate As Bank.UpdateReceipt If spmPropNextReceipt.Value >= spmPropMaxNum.Value Then Set objReceiptUpdate = ctxObject.CreateInstance("Bank.UpdateReceipt") spmPropNextReceipt.Value = objReceiptUpdate.Update(strResult) spmPropMaxNum.Value = spmPropNextReceipt.Value + 100 End If ' Get the next receipt number and update property spmPropNextReceipt.Value = spmPropNextReceipt.Value + 1
Microsoft Transaction Server сочетает в себе функции монитора транзакций и брокера объектных запросов. Как монитор транзакций MTS управляет транзакциями, проходящими через несколько менеджеров ресурсов, распределителями ресурсов (ODBC-соединения) и общими свойствами, процессами и потоками. Как брокер объектных запросов MTS управляет распределением компонент по компьютерам, использованием (в том числе повторным) экземпляров объектов, а также правами и безопасностью объектных вызовов. Приложения пишутся как однопользовательские, оформляются как ActiveX dll'и, регистрируются в среде управления MTS и начинают работать в многопользовательском режиме. Программирование для MTS не требует интенсивного знания COM или Win32 API. Компоненты для MTS могут быть разработаны с использованием широкого перечня средств разработки как от Microsoft, так и от других фирм. MTS поддерживает толстых (Win32 через DCOM) и тонких (броузер через HTTP и ASP) базовых клиентов. Несмотря на сравнительно недавний срок выхода MTS успел зарекомендовать себя как мощное и надежное средство построения и диспетчеризации ПО промежуточного слоя, отвечающее самым современным требованиям концепции распределенных вычислений.
Алексей Шуленин
Microsoft