В данной статье будет рассмотрен вопрос включения 1С в глобальные транзакции в рамках сервис-ориентированной архитектуры. Эта статья является продолжением статьи "Интеграция 1С с сервисной шиной OpenESB".
Введение
Механизм транзакций является ключевым элементом, для построения надежных и отказоустойчивых информационных систем. Всем хорошо известна его полезность в реляционных СУБД - именно транзакции позволяют строить надежные системы, согласовано меняющие состояние и откатывающиеся, в случае сбоя, к предыдущему непротиворечивому состоянию. Ключевым свойствами транзакций, позволяющими все это осуществить, являются: атомарность, согласованность, изолированность и устойчивость(для их обозначения чаще используется аббревиатура ACID).
Гораздо менее известен тот факт, что транзакции могут быть связаны с ресурсами отличными от традиционных баз данных. В общем случае транзакционным может быть назван любой ресурс, который поддерживает:
- Явное или неявное начало транзакции, как группы согласованных действий.
- Отмену всех действий в рамках транзакции и возвращение ресурса в исходное состояние.
- Фиксацию всех действий, которые приводят ресурс к новому допустимому состоянию.
Ну и, разумеется, в случае параллельного доступа к ресурсу должны поддерживаться сразу несколько транзакций, при этом для разделения доступа обычно используются механизм блокировок или версий.
В качестве примера, системы обладающей транзакционностью, можно привести очереди сообщений. Причем в этом случае транзакции для отправителя и для получателя будут представлены по разному: "будет отправлено все или ничего" и "будет получено и удалено из очереди все или ничего".
Все основные программные платформы (а к ним я отношу прежде всего Java и .NET) пришли к осознанию важности обобщенного механизма управления разнообразными транзакционными ресурсами, и, что самое интересное, к согласованному управлению несколькими транзакционными ресурсами в рамках одной транзакции. В Жабе 2 Йо-Йо есть JTS (Java Transaction Service), а в дотнете свой механизм, гнездящийся в пространстве System.Transactions.
Немного терминологии:
- транзакция с участием одного транзакционного ресурса и без двухфазного протокола подтверждения называется "локальной"
- транзакция с несколькими транзакционными ресурсами и двухфазным протоколом подтверждения называется "глобальной".
- "двухфазный протокол подтверждения" предусматривает подтверждение в две фазы: подготовку к подтверждению и собственно подтверждение, разумеется, если какой-то из ресурсов упал с ошибкой, и общего подтверждения транзакции не случилось, ни первая, ни вторая фаза не будет выполнена.
Критичным местом для двухфазного протокола является стадия окончательного подтверждения: если на провод сядет птичка, то в Виллариба может оказаться одно, а в Виллабаджио совсем другое, поэтому-то и выделена в отдельную фазу подготовка. В фазе окончательного подтверждения не должно быть никаких тяжелых действий, она должна быть максимально короткой и с как можно более низкой вероятностью возникновения ошибок.
В общем случае, механизм который рулит транзакцией называется "менеджером транзакции" (координатором), а рулежка транзакции сводится к рулежке транзакционными ресурсами, но не напрямую, а через некий адаптер, называемый "менеджером ресурса". Тут надо понимать, что терминология эта достаточна условна, и в другом месте вполне может использоваться термин "распределенные" вместо "глобальные" и пр.
Наличие промежуточного слоя в виде менеджера ресурса позволяет включать в транзакцию самые разнообразные ресурсы, реализуя соответствующий менеджер. В .NET менеджер ресурса представляет собой по сути обработчик, который слушает состояние транзакции и при наступлении тех или иных событий предпринимает соответствующие действия в отношении подопечного ресурса. Для примера, рассматриваемого в этой статье, создание такого менеджера ресурса для 1С будет играть ключевую роль - именно он позволит включать 1С в глобальные транзакции.
В целом, схема такой глобальной транзакции будет иметь вид:
Природа же координатора нас волновать не будет.
Общие соображения
Как ясно из введения, 1С поднятая через V81.Application прямо таки просится в качестве транзакционного ресурса. Тем более, что и делать-то ничего не надо - поддержка транзакций в 1С уже есть и все, что нужно сделать - это менеджер, который сможет управлять ими и при этом будет совместим с механизмом транзакций .NET'а.
В качестве основы будет использован пример универсального адаптера OneCService из прошлой статьи, потребуется его только слегка переработать и дополнить. Поэтому детально OneCService здесь рассматриваться не будет.
Доработки в основном коснутся:
- Сервиса: нужно включить поддержку транзакций и включение транзакционного ресурса (1C через OneCAdapter) в транзакцию.
- OnceAdapter'а: нужно добавить методы для управления транзакциями.
- Создания менеджера, который будет управлять OneCAdpater'ом, и, как следствие, 1С'ом.
При этом полная поддержка двухфазного протокола в менеджере ресурса реализована не будет - стадия подготовки будет оставлена пустой, так как сам ресурс (1С) не поддерживают двухфазное подтверждение. Также будет предпринят ряд шагов предотвращающий:
- Потерю транзакции - дотнетовский менеджер транзакций там чего-то мудрит, так что периодически вылезала ошибка с неактивной транзакцией. Решение простое: не хранить OneCAdapter в менеджере ресурсов, а хранить его в параметрах домена .NET, использую в качестве ключа GUID, уникальный для экземпляра менеджера ресурсов.
- Потерю ссылкой на V81.Application своей рантайм-обертки (RCW) - проявляется когда менеджер ресурсов получает команду завершить или откатить транзакцию, происходит это уже за рамками пользовательского кода, так что точная причина не понятна. Решение состоит в том, чтобы хранить также неуправляемый указатель на экземпляр V81.Application и в методах завершения/отмены транзакции восстанавливать по нему RCW.
Также не будет реализовано повторное использование V81.Application - пул соединений. На каждый запрос буде подниматься свое соединение с 1С и включаться в транзакцию («любите ли Вы дедлоки так, как люблю их я?»).
Инструментарий
1. 1С 8.1 с файловой БД.
2. Sharpevelop 3.1 + .NET 3.5 SP1 + Windows SDK (там есть svcutil.exe, нужный для построения клиента к сервису).
Реализация
Начнем с некоторых изменений, которые необходимо внести в сервис для включения поддержки транзакций:
[ServiceContract(Name="onecservice", Namespace="http://onecservice")]
public interface IOneCWebService
{
[OperationContract(Name="ExecuteRequest")]
[TransactionFlow(TransactionFlowOption.Mandatory)]
ResultSet ExecuteRequest(string _file, string _usr, string _pwd, string _request);
[OperationContract(Name="ExecuteScript")]
[TransactionFlow(TransactionFlowOption.Mandatory)]
ResultSet ExecuteScript(string _file, string _usr, string _pwd, string _script);
[OperationContract(Name="ExecuteMethodWithXDTO")]
[TransactionFlow(TransactionFlowOption.Mandatory)]
ResultSet ExecuteMethodWithXDTO(string _file, string _usr, string _pwd, string _methodName, XmlNode[] _parameters);
}
TransactionFlow.Mandatory гарантирует обязательное присутствие транзакции с клиентской стороны, иначе клиент получит отлуп, поэтому, кстати, данный пример не совместим с клиентами для изначального OneCService'а.
Перейдем к менеджеру ресурса, который позволит включать 1С в транзакцию, как видно из кода он представляет собой обычной обработчик, реагирующий на изменение состояния транзакции(код приведен с сокращениями):
public class V8ResourceManager : IEnlistmentNotification, IDisposable
{
private Guid resourceGuid = Guid.NewGuid();
private AppDomain domain = null;
public AppDomain Domain
{
set {.....}
get {.....}
}
public OneCAdapter Adapter
{
set {......}
get {......}
}
public Guid ResourceGuid
{
......
}
public void Prepare(PreparingEnlistment preparingEnlistment)
{
......
}
public void Commit(Enlistment enlistment)
{
try
{
//Console.WriteLine("Commit GUID:" + resourceGuid);
Adapter.Commit();
enlistment.Done();
}
catch (Exception _e)
{
SimpleLogger.DefaultLogger.Severe("Error on commit: "+_e.ToString());
}
finally
{
TryClose();
}
}
public void Rollback(Enlistment enlistment)
{
.....
}
public void InDoubt(Enlistment enlistment)
{
.....
}
.....
}
Рассмотрим код, обеспечивающий включений 1С в транзакцию:
private void EnlistToTransaction(OneCAdapter _adapter)
{
if (Transaction.Current != null)
{
V8ResourceManager manager = new V8ResourceManager();
manager.Domain = AppDomain.CurrentDomain;
manager.Adapter = _adapter;
Transaction.Current.EnlistDurable(manager.ResourceGuid, manager, EnlistmentOptions.None);
_adapter.Begin();
}
else
{
Exception e = new Exception("Ambient transaction not found!");
SimpleLogger.DefaultLogger.Severe(e.ToString());
throw e;
}
}
Все что теперь остается сделать - это добавить включение в транзакцию в каждый метод веб-сервиса и можно приступать к созданию клиента.
Значительная часть в создании клиента приходится на автоматическую генерацию клиентского прокси для доступа к сервису, для чего потребуется svcutil.exe (есть в Windows SDK). bat-файл строящий прокси приведен в клиентском проекте, в архиве. Также будет сформирован конфигурационный файл, содержащий, в том числе, и настройки соединения с сервисом. Имея готовый прокси и конфигурацию к нему можно переходить к написанию клиента.
Рассмотрим код клиента:
public static void Main(string[] args)
{
using (TransactionScope t = new TransactionScope(TransactionScopeOption.Required))
{
onecserviceClient client = new onecserviceClient();
try
{
//Первая база 1С через веб-сервис
ResultSet resultSet = client.ExecuteScript(
"C:\Work\OneCService\Base\First",
"", "",
"сотр = Справочники.Сотрудники.НайтиПоКоду(10);\n" +
"Если Не сотр.Пустая() Тогда сотр.ПолучитьОбъект().Удалить(); КонецЕсли;" +
"сотр = Справочники.Сотрудники.СоздатьЭлемент();\n" +
"сотр.Код = 10; " +
"сотр.Наименование = \""; " +
"сотр.Записать();"
);
if (!resultSet.Error.Equals(""))
{
Console.WriteLine("Error: "+resultSet.Error);
throw new Exception(resultSet.Error);
}
//Вторая база 1С через веб-сервис
resultSet = client.ExecuteScript(
"C:\Work\OneCService\Base\Second",
"", "",
"сотр = Справочники.Сотрудники.НайтиПоКоду(10);\n" +
"Если Не сотр.Пустая() Тогда сотр.ПолучитьОбъект().Удалить(); КонецЕсли;" +
"сотр = Справочники.Сотрудники.СоздатьЭлемент();\n" +
"сотр.Код = 10; " +
"сотр.Наименование = \""; " +
"сотр.Записать();"
);
if (!resultSet.Error.Equals(""))
{
Console.WriteLine("Error: "+resultSet.Error);
throw new Exception(resultSet.Error);
}
//Завершение транзакции
t.Complete();
//try
finally
{
client.Close();
Console.ReadKey();
}
//using TransactionScope
}
Как видно из кода, клиент, рассмотренный в этом примере будет работать с двумя базами 1С, в каждой из которых есть справочник "Сотрудники". В ходе работы клиента в этот справочник будет добавляться Сиськин с кодом 10. Весь процесс будет происходить в глобальной транзакции, то есть, если при добавлении во вторую базу произойдет ошибка, то Сиськин не должен будет появится и в первой базе, несмотря на то, что скрипт уже выполнен. Хочется также отметить, что выполнения каждого скрипта будет осуществляться через отдельный вызов сервиса (через HTTP) и, в общем случае, эти сервисы могут находится на разных узлах сети и обеспечивать взаимодействие с разными базами 1С.
Таким образом пример соответствует схеме:
Как обычно, видео с демонстрацией лежит здесь (не забываем переключаться в HD)
Архив с исходниками, бинарниками и базами прилагается.
Недостатки реализации
- Отсутствие пула поднятых V81.Application (в разрезе баз и пользователей, с обязательной проверкой пароля).
- Очень просто организовать блокировку, а если постараться, то и дедлок. Причем в отличии это предыдущей версии это все может быть растянуто по времени и перемешено с другим вызовами (как других баз 1С, так и обычных СУБД или удаленных сервисов).
Лучшее средство от головы... или неполиткорректные мысли о глобальных транзакциях
Распределенные транзакции - это конечно замечательно, но у всего есть свои недостатки и своя обратная сторона. В случае с транзакциями главная опасность кроется глубоко внизу - в механизме разделения доступа к ресурсу. По большому счету таких механизмов два: блокировка и версионность.
С блокировкой все более или менее понятно: при параллельном доступе к ресурсу первый блокирует его, а все последующие ждут. Минусы состоят именно в том, что остальные ждут (любителям клюшек должны быть знакомы тормоза из-за блокировки таблицы журнала документов). И переключение на READ_UNCOMMITED ничего хорошего тоже не даст.
Версионный механизм решает эту проблему: любой, кто изменяет ресурс работает со своей версией ("пишущие не блокируют читающих"). Но и тут есть свои минусы - вилка, которая возникает когда кто-то начал модификацию раньше чем сосед, а завершил позже. Тут в качестве примера можно привести СУБД Firebird.
В случае же с глобальной транзакцией все эти типы разделения доступа и, соответсвенно, все проблемы могут сойтись в одном месте.
Безусловно с такими негативными эффектами можно и нужно бороться, например, устанавливая таймаут на время жизни транзакции, использую промежуточные БД, синхронизируемые с основными, перестраивая схему взаимодействия и другими способами. Но тем не менее, серебряной пули нет и у любого преимущества всегда есть обратная сторона, глобальные транзакции здесь не исключение. Хотя безусловно этот механизм крайне интересен и позволяет строить весьма сложные системы.
Литература
1. Джувел Лёве «Создание служб WCF», O'REILLY/Питер.
P.S. Этот пример, как раз и иллюстрирует почему я так скептически отношусь ко встроенному движку веб-сервисов, который есть в 1С - такая задача ему не по зубам, равно как множество других вещей, например, поддержка стандарта WS-Security и пр. Причем я и не могу сказать, что эти вещи не нужны - движки, реализующие широкий набор стандартов, уже есть и они массово распространяются (WCF у каждого обладателя Висты и 7, Metro в каждом сервере приложений GlassFish). В конечном счете веб-сервисы становятся преобладающим средством взаимодействия и тут уже маркетинговой галочкой "поддержка веб-сервисов в 1С:Предприятие 8.1" не обойтись. Как обычно жду полезной критики и обсуждения.