Недавно, на одном интервью меня спросили, а работал ли я с распределенными транзакциями, в том смысле, что нужно было делать вставку/обновление таких записей при условии:
- Одной транзакции.
- Это могут быть несколько разнообразных баз данных таких как Oracle, MS SQL Server и PostgreSQL.
- Отклик на CRUD операцию может быть значительным.
- Последовательность вставки не важна.
Чего коллеги добивались задавая этот вопрос? Проверить мой опыт или получить готовое решение? На самом деле для меня не важно, но вот что важно — задача в теории вопроса показалась мне интересной и, я решил написать статью о том, как можно было бы решить эту задачу.
Прежде чем я опишу свое решение, вспомним, как реализуется типичная распределенная транзакция на примере платформы Microsoft.
Опция №1. Приложение С++ и ODBC драйвер (вызов Координатора распределенных транзакций Microsoft (MSDTC) для SQL Server по-владению)
Координатор распределенных транзакций Microsoft (MSDTC) позволяет приложениям расширять или распределять транзакции по двум или более экземплярам SQL Server. Распределенная транзакция работает, даже если два экземпляра размещены на разных компьютерах.
MSDTC работает для Microsoft SQL Server только локально, и недоступен для облачной службы базы данных Microsoft Azure SQL.
MSDTC вызывается драйвером собственного клиента SQL Server для Open Database Connectivity (ODBC), когда ваша программа C++ управляет распределенной транзакцией. Драйвер ODBC для собственного клиента имеет диспетчер транзакций, совместимый со стандартом XA Open Distributed Transaction Processing (DTP). Это соответствие требуется MSDTC. Как правило, все команды управления транзакциями отправляются через этот драйвер ODBC для собственного клиента. Последовательность следующая:
Приложение ODBC для собственного клиента C ++ запускает транзакцию, вызывая SQLSetConnectAttr с отключенным режимом автоматической фиксации.
Приложение обновляет некоторые данные на SQL Server X на компьютере A.
Приложение обновляет некоторые данные на SQL Server Y на компьютере B.
В случае сбоя обновления на SQL Server Y все незафиксированные обновления в обоих экземплярах SQL Server откатываются.
Наконец, приложение завершает транзакцию, вызывая SQLEndTran (1) с параметром SQL_COMMIT или SQL_ROLLBACK.
(1) MSDTC может быть вызван без ODBC. В таком случае MSDTC становится менеджером транзакций, и приложение больше не использует SQLEndTran.
Только одна распределенная транзакция
Предположим, что ваше приложение ODBC для собственного клиента C ++ зачислено в распределенную транзакцию. Затем приложение зачисляется во вторую распределенную транзакцию. В этом случае драйвер ODBC собственного клиента SQL Server покидает исходную распределенную транзакцию и включается в новую распределенную транзакцию.
Подробнее о MSDTC можно прочитать тут.
Опция №2. Приложение C# как альтернатива для базы данных SQL в облаке Azur
MSDTC не поддерживается ни для базы данных SQL Azure, ни для хранилища данных SQL Azure.
Однако распределенную транзакцию можно создать для базы данных SQL, если ваша программа на C # использует класс .NET System.Transactions.TransactionScope.
В следующем примере показано, как использовать класс TransactionScope для определения блока кода для участия в транзакции.
static public int CreateTransactionScope(
string connectString1, string connectString2,
string commandText1, string commandText2)
{
// Инициализирует возвращение значения ноль и создает StringWriter для отображения
// результата.
int returnValue = 0;
System.IO.StringWriter writer = new System.IO.StringWriter();
try
{
// Создать TransactionScope чторбы выполнить кооманды, гарантирующие
// что обе коммандлы могут либо завершиться или отмениться как единая единая
// рабочая единица.
using (TransactionScope scope = new TransactionScope())
{
using (SqlConnection connection1 = new SqlConnection(connectString1))
{
// Открытие соединения автоматом включает его в
// TransactionScope как облегченную транзакцию.
connection1.Open();
// Создать объект SqlCommand и выполнить первую команду.
SqlCommand command1 = new SqlCommand(commandText1, connection1);
returnValue = command1.ExecuteNonQuery();
writer.WriteLine("Записи изменены по command1: {0}", returnValue);
// Если программа дошла до этой точки, это означает, что
// команда command1 выполнена успешно. Путем вложения
// оператора блока using для connection2 внутри соединения connection1,
// вы сохраняете сервер и сетевые ресурсы когда connection2 открыто
// только и есть шанс завершить транзхакцию.
using (SqlConnection connection2 = new SqlConnection(connectString2))
{
// Транзакция будет продолжена до полной рпаспределенной транзакции
// когда соединение connection2 открыто.
connection2.Open();
// Выполнить вторую команду во второй БД.
returnValue = 0;
SqlCommand command2 = new SqlCommand(commandText2, connection2);
returnValue = command2.ExecuteNonQuery();
writer.WriteLine("Записи будут изменены по command2: {0}", returnValue);
}
}
scope.Complete();
}
}
catch (TransactionAbortedException ex)
{
writer.WriteLine("Транзакция отменена с ошибкой: {0}", ex.Message);
}
Console.WriteLine(writer.ToString());
return returnValue;
}
Опция №3. Приложения C# и Entity Framework Core для распределенных транзакций.
По умолчанию, если поставщик базы данных поддерживает транзакции, все изменения в одном вызове SaveChanges () применяются в транзакции. Если какое-либо из изменений не выполняется, транзакция откатывается, и ни одно из изменений не применяется к базе данных. Это означает, что SaveChanges () гарантированно либо полностью завершится успешно, либо оставит базу данных неизменной в случае возникновения ошибки.
Для большинства приложений этого поведения по умолчанию достаточно. Вы должны вручную контролировать транзакции только в том случае, если требования вашего приложения сочтут это необходимым.
Вы можете использовать API DbContext.Database для запуска, фиксации и отката транзакций. В следующем примере показаны две операции SaveChanges () и запрос LINQ, выполняемый в одной транзакции.
Не все поставщики баз данных поддерживают транзакции. Некоторые провайдеры могут выдавать или не выполнять операции при вызове API транзакций.
using (var context = new BloggingContext())
{
using (var transaction = context.Database.BeginTransaction())
{
try
{
context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
context.SaveChanges();
context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" });
context.SaveChanges();
var blogs = context.Blogs
.OrderBy(b => b.Url)
.ToList();
transaction.Commit();
}
catch (Exception)
{
}
}
}
Если вы используете EF Core в .NET Framework, реализация System.Transactions там поддерживает распределенные транзакции.
Как видно из документации в рамках платформы и линейки продуктов Microsoft распределенные транзакции не проблема: MSDTC, облако само по себе отказоустойчиво, связка ADO .NET + EF Core для экзотических комбинаций (читать тут).
Как же нам поможет решить задачу обзор транзакций на платформе Microsoft? Очень просто, есть четкое понимание, что стандартного инструментария для реализации распределенной транзакции по разнородным базам данным в приложении .NET просто нет.
А раз так, то делаем собственный home-made координатор распределенных транзакций (КРТ).
Один из вариантов реализации это распределенные транзакции основанные на микросервисах (смотрим тут). Этот вариант не плох, но он требует серьезной доработки Web API для всех систем вовлеченных в транзакцию.
А потому, нам нужен немного другой механизм. Ниже приведен общий подход к разработке КРТ.
Как видно из схемы главной идеей является создание и хранение с обновляемым статусом записи транзакции хранящейся в мастер-базе (или таблице). Даная модель КРТ
позволяет реализовать распределенные транзакции соответствующие требованиям:
- атомарности
- согласованности
- изолированности
- долговечности
КРТ — может быть частью приложения в которой пользователь сформировал запись, а может быть совершенно отдельным приложением в виде системного сервиса. КРТ может содержать специальный подпроцесс который, формирует транзакции в пул на исполнением при отключении электричества (на схеме не указано). Бизнес правила конвертирования (картирования) исходных данных сформированных пользователем для связанных БД могут быть динамически добавляемые и конфигурируемые через XML/JSON формат и храниться в локальной папке приложения, либо в записи транзакции (как вариант). Соответственно для этого случая целесообразно унифицировать конвертирование в связанные БД на уровне кода путем реализации модульности конвертеров в виде DLL. (И да, КРТ подразумевает прямой доступ к БД без участия Web API.)
Таким образом, КРТ в простой форме может быть успешно реализован как часть приложения, или как отдельное настраиваемое и независимое решение (что лучше).
Еще раз уточню что распределенная транзакция это по определению одномоментное сохранение информации без ее изменения, но с возможностью картирования в схемы данных различных баз данных. Так что если вам нужно при фиксировании инцидентов в приемном покое больницы (пулевое ранение) раскидывать в тот же момент времени данные по другим базам, например, в МВД, ФСБ и страховые компании, то данный подход вам безусловно поможет. Этот же механизм может прекрасно работать в финансовых учреждениях.
Надеюсь этот подход показался вам интересным, пишите что вы об этом думаете, коллеги и друзья!
Автор: Дмитрий Бойко