Сообщений 23    Оценка 2075        Оценить  
Система Orphus

Kernel Transaction Manager

С использованием C#

Автор: Акопов Роман Рубенович
Источник: RSDN Magazine #4-2010
Опубликовано: 06.02.2011
Исправлено: 10.12.2016
Версия текста: 1.0
Основы
Целостность и согласованность данных
Понятие транзакции
Изоляция транзакций
Распределённые транзакции, многофазные фиксации
Транзакции режима ядра
Мотивация
Windows API
.Net Framework
Поддержка Windows XP
Примеры использования
TxF
TxR
MSDTC, TxF и SQL Server
Resource Manager
Заключение
Список литературы

Исходники примеров к статье

Предисловие данной статьи совершенно не складывалось; непонятно как общаться с программистом, не знающим что такое транзакция. Я глубоко уверен, что с транзакциями нельзя познакомиться заочно, прочитав книжку и, тем более, статью. Необходима практика. Определения ниже написаны исключительно для уточнения терминологии и, возможно, освежения знаний, но не для замены нормального процесса обучения. Если вы вообще не знаете, что такое транзакция, то эту статью читать вам рано.

Основы

Целостность и согласованность данных

Целостность данных есть не что иное, как неповрежденность данных. На практике это выливается в довольно примитивные условия.

Не нарушена целостность примитивов. Например, в UTF-строке не будет некорректных последовательностей байт.

Не нарушена целостность структур. Например, не будет индексов выходящих за рамки массива, ссылок на несуществующие объекты.

Целостность данных обеспечивает лишь возможность работы с данными, но не обеспечивает их осмысленность.

Согласованность данных – более строгое ограничение, включающее в себя как целостность, так и дополнительные условия, продиктованные предметной областью, описываемой данными. Например, дата последнего доступа к файлу может быть меньше (раньше) даты последней модификации. Целостность данных не нарушена, обе даты можно считать и они попадают в диапазон разумных дат, скажем, от 2000 до 2020 года, но вот согласованность данных в приведённом примере нарушена.

Понятие транзакции

Транзакция — это группа из одной или нескольких операций, обладающая следующими свойствами.

Последние четыре свойства принято называть требованиями ACID (Atomicity, Consistency, Isolation, Durability).

Как правило, для транзакций определены четыре особые операции: начало (begin), фиксация (commit), откат (rollback) и восстановление (recovery). Начало транзакции — операция, не влияющая на пользовательские данные и служащая для группировки отдельных операций в транзакцию. Фиксация транзакции — операция, обозначающая успешное завершение всех операций в рамках транзакции. После фиксации все изменения, выполненные в рамках транзакции, становятся окончательными и общедоступными. Откат транзакции — операция, обозначающая неуспешное завершение транзакции и отмену всех изменений, внесённых уже выполненными операциями. Откат может быть вызван анализом работы отдельной операции, транзакции в целом или внешними факторами. Восстановление — операция, выполняемая после сбоя, когда на момент инициализации системы часть операций уже выполнена, но транзакция ещё не зафиксирована. В зависимости от разных условий, транзакция может быть как отменена, так и зафиксирована в текущем состоянии.

Изоляция транзакций

Определение выше слишком жёстко регламентирует отношения между параллельно выполняющимися транзакциями. Всегда ли необходимо, чтобы изменения, внесённые одной незавершённой транзакцией, были недоступны другой незавершённой транзакции? На данный вопрос нет однозначного ответа, и поведение определяется принятым в системе уровнем изоляции транзакций. Уровень изоляции может назначаться как для системы в целом, так и для отдельной транзакции. Определены некоторые стандартные уровни изоляции, но поведение конкретной системы определяется только её разработчиками, а поддержка стандартных уровней носит рекомендательный характер. Естественно, наиболее предпочтительна ситуация, когда параллельные транзакции выполнятся как последовательные, абсолютно независимо, не влияя друг на друга. Однако такой высокий уровень изоляции обычно сопряжён с большими накладными расходами и, в целях оптимизации, используется крайне редко. При определении уровней изоляции транзакций часто используется понятие строки (row), но так как данная статья рассматривает использование транзакций не только в рамках СУБД, я буду использовать понятие сущности (entity) — минимально редактируемой единицы данных. Это будет строка в случае СУБД, байт в случае файла и т.д.

Проблемы параллельного доступа к данным

Уровни изоляции

Уровень
изоляции

Потерянное
обновление

Грязное
чтение

Неповторяющееся
чтение

Фантомное
чтение

Чтение незафиксированных данных

Предотвращает

Чтение зафиксированных данных

Предотвращает

Предотвращает

Повторяемое чтение

Предотвращает

Предотвращает

Предотвращает

Упорядочиваемость

Предотвращает

Предотвращает

Предотвращает

Предотвращает

Распределённые транзакции, многофазные фиксации

ПРИМЕЧАНИЕ

Как я ни старался, мне не удалось удовлетворительно сформулировать понятие разных компьютеров. Я остановился на “элементы системы следует считать раздельными компьютерами, если у них нет доступа к когерентной общей памяти”. Хотя противоречий с наблюдаемой реальностью найдено не было, гарантировать безупречность формулировки всё же не возьмусь.

Бывает, что операции, входящие в транзакцию, выполняются на разных компьютерах. Такая транзакция называется распределённой. Распределённость транзакции вызывает ряд проблем. Операции, выполненные в рамках транзакции на одном компьютере, могут быть успешно зафиксированы, но фиксация операций, выполненных на другом компьютере, может быть невозможна. Если не предпринять дополнительных мер, могут нарушаться все условия ACID. Для разрешения данной проблемы может применяться многофазная фиксация. Чаще всего используется двухфазная фиксация.

Типичная система, поддерживающая двухфазные фиксации, включает в себя координатор транзакций, которым обычно является один из участников распределённой транзакции, например участник, инициирующий распределённую транзакцию. Все компьютеры, участвующие в транзакции, поддерживают с ним связь. Координатор рассылает участникам транзакции параметры операций, которые необходимо исполнить. После того как все участники транзакции оповещают координатор об успешном завершении работы, координатор рассылает участникам транзакции команду начать первую фазу фиксации. Во время первой фазы фиксации состояние системы не меняется, но выделяются все необходимые ресурсы, выполняются все проверки, гарантирующие возможность фиксации. Сообщения об успешном завершении первой фазы передаются координатору. После того как подобное сообщение получено от всех участников транзакции, координатор рассылает участникам транзакции команду начать вторую фазу фиксации. Если хотя бы один из участников прислал сообщение о неготовности провести фиксацию, или связь прервалась, координатор рассылает остальным участникам команду отката транзакции. Во время второй фазы и происходит собственно фиксация, внесённые изменения становятся долговечными. Во время второй фазы используются только ресурсы, выделенные во время первой фазы. Таким образом, фиксация не может быть прервана нехваткой ресурсов. В случае системного сбоя какого-либо участника до конца первой фазы фиксации, транзакция в результате восстановления будет отменена. В случае сбоя после начала второй фазы транзакция в результате восстановления будет зафиксирована. Поведение при автономном восстановлении между концом первой фазы и началом второй не определено. Таким образом, алгоритм двухфазной фиксации – вероятностный, и основывается на пренебрежительной малости промежутка времени между концом первой фазы и началом второй. Для поддержания согласованности данных автономное восстановление может быть запрещено, что фактически запрещает работу в условиях потери связи.

ПРИМЕЧАНИЕ

Неподготовленного читателя может удивить, что работа распределённых транзакций основывается на низкой вероятности потери сообщения в малый промежуток времени. Казалось бы, транзакции должны опираться на более надёжные алгоритмы. В действительности подобная ненадёжность является простым следствием CAP-теоремы.

Теорема CAP, сформулированная Эриком Брюэром, гласит, что из трёх условий: согласованности данных на узлах (consistency), доступности системы после сбоя одного из узлов (availability) и устойчивости к потере связи между узлами (partition tolerance) одновременно можно добиться не более чем двух. Двухфазная фиксация гарантирует согласованность данных и доступность системы, но не устойчивость к потере связи. Данная проблема решается двумя способами. Во-первых, двухфазная фиксация приводит систему в неопределённое состояние с крайне малой вероятностью. Во-вторых, каналы связи между участниками транзакции дублируются.

Описание трёхфазной фиксации и фиксации Кейдара-Долева, пожалуй, выходит за рамки данной статьи, так как применяются подобные алгоритмы весьма редко.

ПРЕДУПРЕЖДЕНИЕ

Да-да, на самом деле это входит в рамки статьи, просто я сам толком не знаю, что это такое. :)

Транзакции режима ядра

Мотивация

Транзакции давно и успешно используются в СУБД. Однако СУБД не являются единственным доступным способом хранения информации. Более того, существует класс задач, для которых использование СУБД не оправдано и создаёт больше проблем, чем решает. С другой стороны понятие транзакции не ограничивается рамками СУБД. В некоторых случаях требуется обеспечить согласованность данных и атомарность изменений при работе с обычными файлами или ключами реестра. Относительно новые версии ОС Windows (Vista, Windows7, 2008 и 2008 R2, предоставляют дополнительные программные интерфейсы для транзакционной работы.

ПРЕДУПРЕЖДЕНИЕ

Заметная часть примеров не будет работать или будет работать с ошибками под Windows Vista без первого пакета обновлений.

Windows API

Транзакционная работа с файлами и реестром обеспечивается новым компонентом — Kernel Transaction Manager (KTM, http://msdn.microsoft.com/en-us/library/bb986748%28VS.85%29.aspx). Хотя он и содержит в названии слово Kernel, его можно использовать и из пользовательского режима. Более того, KTM поддерживает Microsoft Distributed Transaction Coordinator (MSDTC), таким образом, транзакции могут быть распределёнными двухфазными и затрагивать сразу несколько машин.

KTM основывается на Common Log File System (CLFS) и не ограничивает использование транзакций конкретными типами ресурсов. Предоставляются стандартизированные реализации транзакционной работы с файловой системой и реестром. Вполне возможно добавить поддержку новых типов ресурсов самостоятельно. Таким образом, базовые функции KTM не специализированы, и их использование одинаково вне зависимости от типа ресурсов, над которыми производятся транзакции. Более того, в рамках одной транзакции допустимо обращаться к разным типам ресурсов.

TxF

TxF — сокращённое название для Transactional NTFS. Как легко понять из названия, TxF следует применять для атомарного внесения изменений и сохранения согласованности данных в файлах. Это может быть как один файл, обновление которого выполняется в несколько этапов, так и несколько разных независимых файлов. Transactional NTFS — это программный интерфейс, а не новая версия файловой системы. Данные на диске организованы точно так же.

ПРИМЕЧАНИЕ

Согласно документации, поддерживается только один уровень изоляции — read commited.

ПРЕДУПРЕЖДЕНИЕ

Гарантируется целостность данных только для SCSI или SAN оборудования, поддерживающего аппаратный флаг Force Unit Access (FUA). SATA- и PATA-устройства должны обеспечиваться резервным питанием. Отключение аппаратного кеширования командой IOCTL_DISK_SET_CACHE_INFORMATION позволяет обойти данное ограничение, но приводит к существенному падению производительности. Хорошим компромиссом является использование контроллеров с резервной батареей (host bus adapter with battery backup unit).

При использовании TxF следует помнить об ограничениях:

В рамках транзакции можно создавать и удалять файлы, менять их содержимое и атрибуты. Все эти изменения будут отменены в случае отката транзакции. Поддерживается также транзакционная работа с жёсткими и символическими ссылками.

ПРЕДУПРЕЖДЕНИЕ

TxF может создавать файловый поток с именем «$TXF_DATA:$LOGGED_UTILITY_STREAM» для внутренних нужд.

Следует отметить особую возможность — миниверсии. В рамках транзакции файл можно повторно открыть на чтение в состоянии до начала транзакции. Данная возможность позволяет выполнять сложные обновления файла на основе его текущего содержимого, без создания временных файлов или угрозы нарушения согласованности данных.

Программный интерфейс для работы с файлами претерпел минимальные изменения. Для всех функций, возвращающих дескрипторы файлов, либо не работающих с дескрипторами, созданы аналоги с суффиксом «Transacted» и дополнительным последним параметром — дескриптором транзакции. Если существовало несколько вариантов функции (MoveFile, MoveFileEx, MoveFileWithProgress), то аналог создавался для наиболее функционального, а текущий суффикс имени (WithProgress) отбрасывался. Функции, использующие дескриптор, файла остались без изменений. Таким образом, для транзакционной работы с файлом достаточно открыть его функцией CreateFileTransacted, в то время как запись и чтение производятся без изменений, вызовами ReadFile и WriteFile.

ПРИМЕЧАНИЕ

TxF используется в системных компонентах Windows Update и System Restore. Доступность TxF определяется версией ОС и типом файловой системы.

TxR

TxR — сокращённое название для Transactional Registry. Программный интерфейс претерпел похожие изменения: добавлено минимальное количество функций с суффиксом «Transacted» создающих дескрипторы для транзакционной работы. В отличие от TxF, добавлено два параметра: дескриптор транзакции и некий зарезервированный pExtendedParemeter. Понять его назначение путём логических рассуждений мне не удалось. Кроме того, функции групповой обработки, такие как RegDeleteTree, не имеют транзакционных версий.

Надо заметить, что TxR, в отличие от TxF, не предназначен для повседневного использования. Основные пользователи TxR — системы инсталляции и конфигурации приложений. Учитывая, что TxR и TxF могут участвовать в одной KTM-транзакции, а также, что создание новых файлов в TxF не требует существенных ресурсов, становится возможной установка сложного приложения, включающая в себя копирование множества файлов и создание множества ключей реестра, например для регистрации COM-компонентов, в рамках одной единственной транзакции.

Долговечность изменений в реестре после завершения транзакции вызывает сомнения. Документированный способ сброса данных на диск — вызов функции RegFlushKey. Он очень ресурсоёмкий и сопровождается полной недоступностью реестра для всех процессов. Простой эксперимент с SysInternals Process Monitor показывает, что после фиксации транзакции, выполняется множество операций записи в обход дискового кэша в файлы, хранящие кусты реестра, но это, конечно, не является гарантией долговечности внесённых изменений.

Уровень изоляции также не документирован. Созданные в транзакции ключи становятся доступными только после фиксации транзакции, но в то же время ключи на пути к изменённому в транзакции ключу можно переименовать. При этом дальнейшие операции в рамках транзакции вернут ошибку ERROR_KEY_DELETED. Фактически, это означает полное отсутствие сколько-нибудь полезной изоляции. В целом TxR, по сравнению с TxF, выглядит существенно менее продуманным и полезным механизмом.

ПРИМЕЧАНИЕ

TxR используется в системных компонентах Windows Update и System Restore. Доступность KTM и TxR определяется только версией ОС.

Transaction Manager

Менеджер транзакций является экземпляром журнала Common Log Filesystem (CLFS). Существует три типа менеджеров транзакций: superior, durable (regular), volatile.

ПРИМЕЧАНИЕ

Хороших переводов для этих терминов не придумалось, так что переводов не будет совсем.

Менеджер типа superior служит для координации работы других менеджеров. Примером superior-менеджера служит Microsoft Distributed Transaction Coordinator (MSDTC).

Менеджер типа durable хранит записи об операциях над ресурсами, возможно, разнотипными.

Менеджер типа volatile не хранит записи об операциях над ресурсами и служит для доступа только для чтения.

Resource Manager

Менеджер ресурсов позволяет производить какие-либо операции над конкретными типами ресурсов. Программный интерфейс KTM ограничен абстракциями, для выполнения операций над конкретными ресурсами используется отдельный специфичный программный интерфейс, например TxF или TxR. Можно создавать новые менеджеры ресурсов. Их можно создавать как для, собственно, управления ресурсами, так и для получения оповещений о статусе транзакций.

Enlistment (связывание)

Связывание служит для указания участия менеджера ресурсов в транзакции и позволяет менеджеру ресурсов получать оповещения о состоянии транзакции.

.Net Framework

Официального программного интерфейса для .Net не существует. Хотя это и открывает простор для творчества, очевидным негативным эффектом является необходимость прав Code Access Security на выполнение неуправляемого кода. Это обычно не большая проблема для клиентских и серверных приложений, но может создать трудности при размещении Web-приложений у провайдера. В данной статье будет использоваться библиотека Nabu. На это есть две причины. Во-первых, никаких аналогов, то есть полноценных обёрток, найти не удалось. Во-вторых, автор статьи и автор библиотеки полностью совпадают.

ПРИМЕЧАНИЕ

Mercurial репозиторий располагается по адресу https://dev.triflesoft.org/mercurial/nabu/

Nabu лицензируется согласно трёхпунктовой лицензии BSD.

К проекту потребуется подключить модули Nabu, Nabu.Platform, Nabu.IO.

В примерах к библиотеке можно найти дополнительный учебный код.

Поддержка Windows XP

Возникает логичный вопрос — как писать приложение, которое будет использовать транзакции на тех версиях Windows, на которых они доступны, но будет работать и на более ранних версиях? Разработчики API несколько помогли в этом, предоставив новые функции только для создания дескрипторов ресурсов, но не для работы с ними. Таким образом, хотя и придётся применять условные операторы, их достаточно легко локализовать, воспользовавшись шаблонами проектирования «абстрактная фабрика» и «фабричный метод».

Примеры использования

Ну что же, самое время перейти от скучной теории к скучной практике. Для начала хотелось бы явно обозначить несколько особенностей реализации работы с KTM в библиотеке Nabu. Хотя KTM легко интегрируется с MSDTC, в библиотеке Nabu методы не пытаются найти текущую транзакцию, например, через свойство Transaction.Current, и неявно к ней подключиться. Это поведение преднамеренно и обусловлено тем фактом, что операции с файлами – слишком часто используемый механизм, и автоматическое неявное включение в транзакцию файловых операций может привести к трудно отслеживаемым ошибками и принести существенно больше вреда, чем пользы.

В статье будут рассмотрены только наиболее интересные отрывки из исходного кода примеров. Полный исходный код примеров поставляется отдельно.

TxF

Предполагается, что объявлены следующие переменные:

        string rootPath = "Путь к доступному для записи каталогу на NTFS-разделе";
byte[] ABCDEFGH = Encoding.ASCII.GetBytes("abcdefgh");
byte[] IJKLMNOP = Encoding.ASCII.GetBytes("IJKLMNOP");

Создание файла

Для транзакционного создания файла используется специальная новая функция, но для дальнейших операций используются существующие. Это позволяет не переписывать весь код с нуля. Так, метод FileSystem.Create создаёт объект типа TransactedFileStream, который является наследником стандартного типа FileStream.

ПРИМЕЧАНИЕ

Классы KtmTransaction, FileSystem и TransactedFileStream, как и перечисление FileAccessRights, объявлены в библиотеке Nabu.

ПРЕДУПРЕЖДЕНИЕ

По техническим причинам класс TransactedFileStream предоставляет свойство Path взамен свойству Name базового класса FileStream.

          string path = Path.Combine(rootPath, @"test-0.txt");

using (KtmTransaction transaction = new KtmTransaction("Create "))
{
  using (Stream stream =
    FileSystem.CreateFile(
transaction,
      path,
      FileMode.Create,
      FileAccessRights.GenericWrite))
  {
    stream.Write(ABCDEFGH, 0, ABCDEFGH.Length);
  }

  transaction.Rollback();
  // Когда я пришёл, так и было.
}

Параметр конструктора KtmTransaction – строковое описание транзакции, и не обрабатывается программно. Результатом работы данного кода будет… ничего. Результата работы нет, так как транзакция была откачена. Если рассмотреть код внимательнее, то можно обратить внимание на одну особенность — необходимые права доступа задаются новым перечислением FileAccessRights, а не FileAccess. Перечисление FileAccessRights добавлено не случайно, оно более точно отражает программный интерфейс Win32 и позволяет запрашивать минимально необходимые права доступа. Перегрузки с перечислением FileAccess реализованы, но в примерах они не используются. Если вместо метода Rollback() вызвать Commit(), то будет создан файл, содержащий текст “abcdefgh”.

Чтение миниверсий

Как уже упоминалось, миниверсии позволяют читать содержимое файла, существовавшее до начала транзакции.

          string path = Path.Combine(rootPath, @"test-0.txt");

File.WriteAllText(path, "abcdefgh");

using (KtmTransaction transaction =
  new KtmTransaction("Write, ReadMiniVersion, Rollback"))
{
  using (Stream stream =
    FileSystem.CreateFile(
      transaction,
      path,
      FileMode.Create,
      FileAccessRights.GenericWrite))
  {
    stream.Write(IJKLMNOP, 0, IJKLMNOP.Length);
  }

  using (Stream stream =
    FileSystem.CreateFile(
      transaction,
      path,
      FileMode.Open,
      FileAccessRights.GenericRead,
      FileShare.ReadWrite,
      4096,
      FileAttributes.Normal,
      FileOptions.None,
      null,
      FileMiniVersion.Dirty))
  {
    byte[] data = newbyte[8];

    stream.Read(data, 0, data.Length);

    Console.WriteLine(
      "Write, Read(Dirty): {0}",
      Encoding.ASCII.GetString(data));
  }

  using (Stream stream =
    FileSystem.CreateFile(
      transaction,
      path,
      FileMode.Open,
      FileAccessRights.GenericRead,
      FileShare.ReadWrite,
      4096,
      FileAttributes.Normal,
      FileOptions.None,
      null,
      FileMiniVersion.Commited))
  {
    byte[] data = newbyte[8];

    stream.Read(data, 0, data.Length);

    Console.WriteLine(
      "Write, Read(Commited): {0}",
      Encoding.ASCII.GetString(data));
  }

  transaction.Rollback();
}

При исполнении в консоль будет выведено:

Write, Read(Dirty): IJKLMNOP
Write, Read(Commited): abcdefgh

Хотелось бы обратить внимание на права доступа, запрашиваемые при открытии миниверсии — FileAccessRights.GenericRead. Если запросить права на запись, в доступе будет отказано, так как миниверсии доступны только для чтения.

Создание миниверсий

Да, подзаголовок не врёт, миниверсии можно создавать! В любой момент времени можно сделать снимок текущего состояния файла, обновляемого в рамках транзакции. Делается это довольно просто.

          string path = Path.Combine(rootPath, @"test-0.txt");

using (KtmTransaction transaction =
  new KtmTransaction("Write, ReadCustomMiniVersion, Rollback"))
{
  using (TransactedFileStream stream =
    FileSystem.CreateFile(
      transaction,
      path,
      FileMode.Open,
      FileAccessRights.GenericWrite))
  {
    // Запись до создания миниверсии.
    stream.Write(IJKLMNOP, 0, IJKLMNOP.Length/2);

    using (Stream miniStream =
      FileSystem.CreateFileMiniVersion(transaction, stream))
    {
      // Запись после создания миниверсии.
      stream.Write(IJKLMNOP, IJKLMNOP.Length/2, IJKLMNOP.Length/2);

      byte[] data = newbyte[8];

      miniStream.Read(data, 0, data.Length);

      Console.WriteLine(
        "Write, Read(Custom): {0}",
        Encoding.ASCII.GetString(data));
    }
  }

  transaction.Commit();
}

Console.WriteLine("Write, Read(Result):   {0}", File.ReadAllText(path));

При исполнении в консоль будет выведено:

Write, Read(Custom): IJKLefgh
Write, Read(Result): IJKLMNOP

Перемещение файла

          string oldPath = Path.Combine(rootPath, @"test-0.txt");
string newPath = Path.Combine(rootPath, @"test-1.txt");

using (KtmTransaction transaction =
  new KtmTransaction("Move, Commit "))
{
  FileSystem.MoveFile(
    transaction,
    oldPath,
    newPath,
    MoveFileFlags.ReplaceExisting);

  // Файл ещё доступен по старому пути.
  transaction.Commit();
  // Файл уже доступен по новому пути.
}
СОВЕТ

Типичным сценарием использования TxF является обновление множества файлов. В таком случае необходимо создать файлы на том же разделе (Create), но с другими именами. Наполнить новые файлы содержимым (Write), после чего переместить (Move) с замещением новые файлы поверх старых. В случае отката транзакции все файлы будут сохранены в своём изначальном состоянии. Данный метод обновления приведёт к минимальному расходу системных ресурсов.

Удаление файла

          string path = Path.Combine(rootPath, @"test-1.txt");

using (KtmTransaction transaction =
  new KtmTransaction("Delete, Commit"))
{
  FileSystem.DeleteFile(transaction, path);

  // Файл ещё доступен.
  transaction.Commit();
  // Файл уже не доступен.
}

Создание нескольких файлов

Естественно, TxF был бы не так полезен, если бы в рамках одной транзакции можно было редактировать только один файл. Объединить операции над несколькими файлами в одну транзакцию довольно просто.

          string path1 = Path.Combine(rootPath, @"test-3.txt");
string path2 = Path.Combine(rootPath, @"test-4.txt");

using (KtmTransaction transaction = new KtmTransaction("MultiFile"))
{
  using (Stream stream =
    FileSystem.CreateFile(
      transaction,
      path1,
      FileMode.Create,
      FileAccessRights.GenericWrite))
  {
    stream.Write(ABCDEFGH, 0, ABCDEFGH.Length);
  }

  using (Stream stream =
    FileSystem.CreateFile(
      transaction,
      path2,
      FileMode.Create,
      FileAccessRights.GenericWrite))
  {
    stream.Write(IJKLMNOP, 0, IJKLMNOP.Length);
  }

  // Нет ни одного файла.
  transaction.Commit();
  // Есть оба файла.
}

Хотя это и не имеет прямого отношения к KTM, хотелось бы продемонстрировать некоторые дополнительные возможности.

Каталоги

Создание и удаление каталогов также может выполняться в рамках транзакции.

          string path = Path.Combine(rootPath, @"test-dir");

using (KtmTransaction transaction = new KtmTransaction("Directory"))
{
  FileSystem.CreateDirectory(transaction, path);

  transaction.Commit();
}

Проверка типа файловой системы

TxF, как я уже упоминал, функционирует только на NTFS-разделах. Для улучшенной диагностики ошибок, проверки конфигурации и т.п. может быть полезно уметь определять тип файловой системы. Это можно сделать в три строки кода. Во-первых, для интересующего пути необходимо определить точку монтирования раздела (volume mount point). Разделам, как правило, назначаются буквы дисков, и тогда путь точки монтирования выглядит как «C:\». Но так как букв всего 26, а разделов может быть больше, существует возможность подключать разделы в качестве каталогов других разделов. Более того, у одного раздела может быть несколько точек монтирования. Раздел может быть смонтирован как каталог на разделе, который в свою очередь смонтирован как каталог. Единственное ограничение – отсутствие циклических зависимостей. Таким образом, вполне возможна ситуация, когда путь «C:\Folder» является каталогом на NTFS-разделе, но «C:\Folder\Subfolder» является каталогом на FAT32-разделе. Код ниже демонстрирует определение пути точки монтирования.

          string mountPoint = FileSystem.GetVolumeMountPoint(testPath); 

Путь точки монтирования по формату является путём к каталогу.

Далее, по пути точки монтирования необходимо получить уникальный путь раздела. Путь раздела – это путь к виртуальному физическому устройству. Он имеет формат:

\\?\Volume{GUIDGUID-GUID-GUID-GUID-GUIDGUIDGUID}\

Код ниже демонстрирует определение пути раздела.

          string guidPath = FileSystem.GetVolumeGuidPath(mountPoint);

Последним шагом является получение информации о разделе по его пути. Надо заметить, что возвращается чуть больше информации, чем необходимо, но это не вызывает проблем, так как всё сводится к одному системному вызову. Код ниже демонстрирует получение информации о разделе.

VolumeInfo volumeInfo = FileSystem.GetVolumeInfo(guidPath);

Если свойство VolumeInfo.FileSystem возвращает строку «NTFS», то раздел отформатирован как NTFS и функциональность TxF будет доступна. Естественно, вне зависимости от типа файловой системы, требования к версии ОС сохраняются.

Получение дополнительной информации

СОВЕТ

Системные администраторы для получения информации о текущем состоянии TxF могут выполнить из командной строки «fsutil resource info %RM_PATH%» или «fsutil transaction list».

Программный интерфейс TxF может предоставлять общесистемную информацию о менеджерах ресурсов, транзакциях, заблокированных файлах. Хотя это и не сказано явно (ну или я не нашёл), исходя из знаний о TxF, становится понятно, что создаётся отдельный менеджер ресурсов для каждого раздела. Это достаточно важный момент, так как для получения информации о состоянии менеджера ресурсов необходимо знать корневой каталог менеджера ресурсов. На практике, корневой каталог менеджера ресурсов — это точка монтирования раздела. Код, представленный ниже, слишком объёмен для абстрактного словесного описания, и пояснения даны в виде комментариев.

          string path1 = Path.Combine(rootPath, @"test-x");
string path2 = Path.Combine(rootPath, @"test-y");

// Создатся первая транзакция. Она не нужна для получения// информации, но из-за неё список транзакций не будет пустым.using (KtmTransaction transaction1 = new KtmTransaction("Directory"))
{
  // Создаётся каталог в рамках транзакции №1.
  FileSystem.CreateDirectory(transaction1, path1);

  // Создатся вторая транзакция. Она не нужна для получения// информации, но из-за неё список транзакций не будет пустым.using (KtmTransaction transaction2 = new KtmTransaction("File"))
  {
    // Создаётся файл в рамках транзакции №2using (Stream stream =
      FileSystem.CreateFile(
        transaction2,
      path2,
      FileMode.Create,
      FileAccessRights.GenericWrite))
    {
      // Коллекция для хранения точек монтирования.
      List<string> mountPoints = new List<string>();

      foreach (VolumeEntry volumeEntry in FileSystem.FindVolumes())
      {
        try
        {
          // Перечисление всех разделов.
          VolumeInfo volumeInfo = FileSystem.GetVolumeInfo(volumeEntry.Path);

          // Если файловая система типа NTFS.if (volumeInfo.FileSystem == "NTFS")
          {
            string[] volumeMountPoints =
              FileSystem.GetVolumeMountPoints(volumeEntry.Path);

            // Перечисление точек монтирования раздела.foreach (string volumeMountPoint in volumeMountPoints)
            {
              mountPoints.Add(volumeMountPoint);
              break;
            }
          }
        }
        catch (NativeException)
        {
          // Раздел может быть недоступен,// например, если это съёмный диск.
        }
      }

      // Перечисление всех ранее найденных точек монтирования.foreach (string mountPoint in mountPoints)
      {
        Console.WriteLine("Volume: {0}", mountPoint);
        Console.WriteLine("Active Transactions:");

        // Все активные транзакции в рамках менеджера ресурсов.
        TransactionEntry[] transactionEntries =
          FileSystem.GetTransactions(mountPoint);

        foreach (TransactionEntry transactionEntry in transactionEntries)
        {
          Console.WriteLine(
            "\t\t{0} = {1}",
            transactionEntry.ID,
            transactionEntry.State);

          TransactionFileEntry[] transactionFileEntries =
            FileSystem.GetTransactionFiles(
              mountPoint,
              transactionEntry.ID);

          // Перечисление всех файлов, участвующих в транзакции.foreach (TransactionFileEntry transactionFileEntry in transactionFileEntries)
          {
            Console.Write("\t\t\t{0}", transactionFileEntry.Path);

            if (transactionFileEntry.IsCreated)
            {
              // Файл был создан в рамках транзакции.
              Console.Write(", Created");
            }

            if (transactionFileEntry.IsDeleted)
            {
              // Файл был удалён в рамках транзакции.
              Console.Write(", Deleted");
            }

            Console.WriteLine();
          }
        }

        // Информация о текущем состоянии менеджера ресурсов.
        TxfResourceManagerEntry resourceManagerEntry =
          FileSystem.GetResorceManagerInfo(mountPoint);

        // В приложенном к статье коде на экран выводится существенно         // больше информации.
        Console.WriteLine("\tTransactions:");
        Console.WriteLine(
          "\t\tCount              = {0}",
          resourceManagerEntry.TransactionCount);
        Console.WriteLine(
          "\t\tOne-phase commits  = {0}",
          resourceManagerEntry.OnePCCount);
        Console.WriteLine(
          "\t\tTwo-phase commits  = {0}",
          resourceManagerEntry.TwoPCCount);
        Console.WriteLine(
          "\t\tOldest Age (ms) = {0}",
          resourceManagerEntry.OldestTransactionAge);

        Console.WriteLine();
      }
    }

    transaction2.Commit();
  }

  transaction1.Commit();
}
СОВЕТ

Перед перечислением разделов рекомендую вызвать метод OSBehavior.SuppressErrorDialogs(), иначе для недоступных сменных носителей пользователю могут быть показаны некрасивые модальные системные диалоги.

Вторичные менеджеры ресурсов

Создаваемые системой менеджеры ресурсов, так называемые "первичные менеджеры ресурсов", обладают двумя важными свойствами. Во-первых, они автоматически перезапускаются при старте ОС. Это означает, что откат незавершённых транзакций влияет на скорость загрузки ОС. Во-вторых, первичные менеджеры в спорных (in doubt) ситуациях предпочитают доступность (availability) целостности (consistency). Это означает, что в случае автономного восстановления транзакция, которая должна была бы быть зафиксирована, может быть откачена в угоду возможности быстрого запуска ОС.

Обе эти особенности могут быть нежелательны при работе с пользовательскими данными. TxF позволяет создавать так называемые вторичные менеджеры ресурсов. Вторичные менеджеры ресурсов необходимо запускать вручную, а приоритеты их работы можно настраивать.

СОВЕТ

Системные администраторы для вторичного менеджера ресурсов TxF могут выполнить из командной строки «fsutil resource create %RM_PATH%».

ПРЕДУПРЕЖДЕНИЕ

Текущая версия Nabu не предоставляет программного интерфейса для управления вторичными менеджерами ресурсов TxF.

TxR

Предполагается, что объявлены следующие переменные

RegistryKey rootKey = Registry.CurrentUserKey;
string path = "Путь к доступному для создания или записи ключу реестра";
ПРЕДУПРЕЖДЕНИЕ

Класс RegistryKey объявлен в библиотеке Nabu в пространстве имён Nabu.Platform.

Создание ключа

          using (KtmTransaction transaction = new KtmTransaction("CreateKey"))
{
  using (RegistryKey key =
    rootKey.Create(transaction, path, RegistryAccessRights.GenericAll))
  {
  }

  transaction.Rollback();
  // Всё зря.
}

Как и в случае с файлами, видимого результата после отката транзакции не будет.

Удаление ключа

          using (KtmTransaction transaction = new KtmTransaction("DeleteKey"))
{
  rootKey.Delete(transaction, path);

  transaction.Commit();
}

Помните, для успешного удаления ключа реестра у него не должно быть подключей.

Задание значения

Транзакционное задание значений не сложно. Достаточно задавать их для ключа, открытого в рамках транзакции.

          using (KtmTransaction transaction = new KtmTransaction("SetValue"))
{
  using (RegistryKey key =
    rootKey.Open(transaction, path, RegistryAccessRights.GenericAll))
  {
    key.Values["Name"] = "Value";
  }

  transaction.Commit();
}
СОВЕТ

Для более тонкой работы используйте свойство RegistryKey.ValueInfos вместо RegistryKey.Values.

Получение значения

TxR не поддерживает миниверсии. Однако можно получить значение, каким оно было до начала транзакции, используя обычный программный интерфейс вместо транзакционного.

          using (RegistryKey key = rootKey.Open(path, RegistryAccessRights.GenericWrite))
{
  key.Values["Name"] = "OldValue";
}

using (KtmTransaction transaction = new KtmTransaction("QueryValue"))
{
  using (RegistryKey key =
    rootKey.Open(transaction, path, RegistryAccessRights.GenericAll))
    key.Values["Name"] = "NewValue";

  using (RegistryKey key =
    rootKey.Open(transaction, path, RegistryAccessRights.GenericRead))
  {
    Console.WriteLine(key.Values["Name"]);
  }

  using (RegistryKey key =
    rootKey.Open(path, RegistryAccessRights.GenericRead))
  {
    Console.WriteLine(key.Values["Name"]);
  }

  transaction.Commit();
}

В результате в консоль будет выведено:

NewValue
OldValue

Удаление значения

          using (KtmTransaction transaction = new KtmTransaction("DeleteValue"))
{
  using (RegistryKey key =
    rootKey.Open(transaction, path, RegistryAccessRights.GenericAll))
  {
    key.Values["Name"] = null;
  }

  transaction.Commit();
}

MSDTC, TxF и SQL Server

Переходим к более сложному примеру — распределённая транзакция над ресурсами разного типа. Для успешной работы необходимо выполнить несколько требований. Во-первых, должна быть запущена служба «Distributed Transaction Coordinator». Это можно сделать как из MMC-оснастки, так и из командной строки

net start msdtc

Если сервис MSDTC будет недоступен, то при попытке использования KTM-транзакции будет сгенерировано исключение:

System.Transactions.TransactionAbortedException
  ("The transaction has aborted.")

И вложенное в него:

System.Transactions.TransactionPromotionException
  ("MSDTC on server '%COMPUTERNAME%' is unavailable.")

Далее, для выполнения примера необходима локальная база данных SQL Server с именем «RSDN_Article_KTM», инициализированная, как показано ниже

        USE [RSDN_Article_KTM]
GO
IFEXISTS
(
  SELECT *
  FROM sys.objects
  WHERE
  object_id = OBJECT_ID(N'[dbo].[TestTable]') AND
  type in (N'U')
)
  DROPTABLE [dbo].[TestTable]
GO
CREATETABLE [dbo].[TestTable]
(
  [Name] NVARCHAR(64) NOTNULL,
  [Value] NVARCHAR(64) NOTNULL
) ON [PRIMARY]
GO

Если придётся расположить базу в другом месте, не забудьте обновить строку подключения в файле App.config. В отличие от примера TxF, код не был перегружен проверкой типа файловой системы, так как он и так великоват для разбора.

        using (TransactionScope transactionScope = new TransactionScope())
{
  stringvalue = DateTime.Now.ToString("yyyy-MMM-dd HH:mm:ss");

  using (SqlConnection sqlConnection = new SqlConnection(connectionString))
  {
    sqlConnection.Open();

    using (SqlCommand sqlCommand = sqlConnection.CreateCommand())
    {
      sqlCommand.CommandText =
        "INSERT INTO [TestTable] VALUES (@Name, @Value)";
      sqlCommand.Parameters.Add(
        new SqlParameter(
          "Name",
          Environment.UserName));
      sqlCommand.Parameters.Add(
        new SqlParameter(
          "Value",
          value));
      sqlCommand.ExecuteNonQuery();
    }
  }

  // KTM-транзакция для текущей DTC-транзакцииusing (KtmTransaction ktmTransaction = KtmTransaction.GetCurrent())
  {
    string desktopPath =
      Environment.GetFolderPath(
        Environment.SpecialFolder.DesktopDirectory);
    string path =
      Path.Combine(
        desktopPath,
        @"test-distributed-transaction.txt");

    using (Stream stream =
      FileSystem.CreateFile(
        ktmTransaction,
        path,
        FileMode.Create,
        FileAccessRights.GenericWrite))
    {
      byte[] FileContent = Encoding.ASCII.GetBytes(value);

      stream.Write(FileContent, 0, FileContent.Length);
    }
  }

  // Фиксация обеих частей транзакции.
  transactionScope.Complete();
}

Хотелось бы разобрать этот пример подробнее. Распределённая транзакция, а если быть точнее, область распределённой транзакции задаётся кодом

        using (TransactionScope transactionScope = new TransactionScope())
{
  ...
  transactionScope.Complete();
}

Обратите внимание, что transactionScope.Complete() — единственная явная команда фиксации. В отличие от предыдущих примеров, у класса KtmTransaction не вызывается метод Commit(). Это будет сделано координатором транзакции, не говоря уже о том, что фиксация будет двухфазной. Если для файлов приходится подключаться к текущей распределённой транзакции явно, получая KTM-транзакцию для распределённой транзакции, то для SQL Server такой необходимости нет. Клиентская библиотека SQL Server автоматически проверяет наличие активной транзакции Transaction.Current и подключается к ней, если она существует. Как я уже упоминал, файловые операции – слишком широко используемый механизм, и автоматическое подключение к распределённой транзакции выглядит весьма спорным решением, и потому не было реализовано.

Советую «поиграть» с примером, вызвать ошибки в SQL- или KTM-части и убедиться, что либо обе части завершаются успешно, либо ни одна из них.

ПРИМЕЧАНИЕ

Сценарием использования распределённой транзакции в рамках одного компьютера может служить загрузка файлов на сервер с сохранением информации о них в базе данных. Достаточно частая операция для приложений, не желающих хранить вложения непосредственно в таблицах. Надо отметить, что у SQL Server есть специальный интерфейс для транзакционной работы со столбцами VARBINARY, фактически хранящимися в файлах – FILESTREAM. С другой стороны, хранилище FILESTREAM всегда располагается там же, где и SQL Server, а это не всегда удобно. Думайте сами, решайте сами…

Resource Manager

Написание своего менеджера ресурсов – задача крайне сложная, требующая глубокого понимания механизма работы транзакций. Я рассмотрю более простой сценарий — фиктивный менеджер ресурсов, получающий оповещения о состоянии транзакции. Подчёркиваю, представленный код является учебным, и не должен применяться как есть в рабочем окружении. Так как даже минимальный пример получился довольно объёмным, пояснения даны прямо в коде, в виде комментариев.

        public
        static
        class Program
{
  // Глобальные переменыне – зло, но я не хотел перегружать пример// вспомогательными классами.privatestatic KtmEnlistment _enlistment;

  // Оповещения менеджера ресурсов необходимо считывать в отдельном потоке.privatestaticvoid ThreadProc(object parameter)
  {
    // Менеджер ресурсов передаётся функции потока как параметр.
    KtmResourceManager resourceManager = (KtmResourceManager)parameter;

    // Выйти из цикла после завершения транзакции.for (bool isComplete = false; !isComplete; )
    {
      // Ждать нового оповещения не более 10 секунд.
      KtmNotificationInfo notificationInfo =
        resourceManager.GetNotification(10000);

      // Если оповещение пришлоif (notificationInfo != null)
      {
        // Порадовать себя диагностическим сообщением.
        Console.WriteLine(
          "{0:x8} - {1}",
          notificationInfo.VirtualClock,
          notificationInfo.Type);

        // Флаг, определяющий, возможно ли успешное// завершение запрошенного действия.bool isWeatherOnMarsFine = true;

        switch (notificationInfo.Type)
        {
          // До первой фазы фиксацииcase KtmEnlistmentNotificationType.PrePrepare:
            if (isWeatherOnMarsFine)
              _enlistment.PrePrepareComplete();
            else
              _enlistment.Rollback();
            break;
          // Первая фаза фиксацииcase KtmEnlistmentNotificationType.Prepare:
            if (isWeatherOnMarsFine)
              _enlistment.PrepareComplete();
            else
              _enlistment.Rollback();
            break;
          // Вторая фаза фиксацииcase KtmEnlistmentNotificationType.Commit:
            if (isWeatherOnMarsFine)
            {
              _enlistment.CommitComplete();
            }
            else
            {
              // Это свинство, и так делать нельзя.// Вторая фаза на то и есть, чтобы// никакие внутренние причины// не могли помешать фиксации.
              _enlistment.Rollback();
            }

            // Транзакция завершена.
            isComplete = true;
            break;
          // Откатcase KtmEnlistmentNotificationType.Rollback:
            _enlistment.RollbackComplete();
            // Транзакция завершена.
            isComplete = true;
            break;
        }
      }
    }
  }

  publicstaticvoid Main(string[] args)
  {
    // Нужен путь к каталогу, доступному на запись.// Рабочий стол вполне подходит.string desktopPath =
      Environment.GetFolderPath(
        Environment.SpecialFolder.DesktopDirectory);

    // Путь к файлу журнала задаётся в NT-формате, с преФиксом \\?\// Преобразование не вполне корректное, но работать в большинстве// случаев будет.using (KtmTransactionManager transactionManager =
      new KtmTransactionManager(@"\\?\" + desktopPath + @"\KTM"))
  {
      // Менеджер ресурсов. using (KtmResourceManager resourceManager =
        new KtmResourceManager(
          // Менеджер транзакций.
          transactionManager,
          // Тип менеджера ресурсов.// Данный GUID не должен меняться при повторном запуске           // менеджера ресурсов.new Guid("{A003795D-6AD9-4F18-8563-0EC7E53177E2}"),
          // Текстовое описание."TestRM"))
      {
        // Поток для ожидания оповещений
        Thread thread = new Thread(ThreadProc);

        thread.Start(resourceManager);

        // Транзакция, в рамках которой будет// использоваться менеджер ресурсов.using (KtmTransaction transaction = 
          new KtmTransaction("TestTransaction"))
        {
          // Менеджер ресурсов включается в транзакцию// и начинает получать оповещения.
          _enlistment = new KtmEnlistment(resourceManager, transaction);

          // Фиксация транзакции.
          transaction.Commit();
        }

        // Если отменить подключение менеджера ресурсов до конца транзакции,// транзакция будет откачена.
        _enlistment.Dispose();
      }
    }
  }
}

Код с собственным менеджером ресурсов был приведён для полноты картины. Придумать разумный сценарий его использования мне не удалось.

Заключение

Спасибо всем, кто помогал, всем, кто не мешал, и всем, кто дочитал.

Список литературы

  1. MSDN
  2. http://www.codeproject.com/KB/vista/KTM.aspx
  3. http://community.bartdesmet.net/blogs/bart/archive/2006/11/05/Windows-Vista-_2D00_-Introducing-TxF-in-C_2300_-_2800_part-1_2900_-_2D00_-Transacted-file-delete.aspx
  4. http://community.bartdesmet.net/blogs/bart/archive/2006/12/15/Windows-Vista-_2D00_-Introducing-TxR-in-C_2300_-_2800_Part-2_2900_.aspx
  5. http://community.bartdesmet.net/blogs/bart/archive/2007/02/21/windows-vista-introducing-txf-in-c-part-3-createfiletransacted-demo.aspx


Эта статья опубликована в журнале RSDN Magazine #4-2010. Информацию о журнале можно найти здесь
    Сообщений 23    Оценка 2075        Оценить