Тема статьи: Репликация базы данных
Автор: Евдокимов Алексей
Контакты: sneg@unn.ac.ru

Редакция от 15.10.2001
Частичная или полная публикация возможна только с согласия автора.




1. Введение


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




2. Special Thanks


      Огромная благодарность Игорю Трофимову. С его разрешения в основу этой статьи легла его терминология и некоторые положения проблемы репликации взятые из его статьи Синхронизация БД




3. Терминология, классификация, конфликты


Репликация (синхронизация) - процесс приведения данных электронных таблиц двух БД в идентичное состояние

Репликацию можно классифицировать по разному. В рамках данной статьи возьму на себя смелость остановиться на следующем варианте:

  I. По направлению репликации. Если данные изменяются только в одной из БД, а в другой данные только хранятся и не подвергаются изменениям, то такую репликацию будем называть однонаправленной или односторонней. Если же данные могут изменяться и вводиться на всех БД, то такой вид репликации будем называть мультинаправленной или многосторонней.
  II. По времени проведения сеанса репликации. Если данные должны быть засинхронизированны немедленно после изменений, то такую репликацию будем называть репликация реального времени. Если же процесс репликации запускается по какому либо событию во времени или по отмашке администратора БД, то такой вид репликации назовем отложенная репликация.
  III. По способу передачи информации во время процесса репликации. Если соединение серверов, хранящих распределенные БД, происходит при помощи программы клиента, которая с одной стороны коннектится к своему серверу, а с другого конца имеет прямую связь с БД другого сервера и может подключиться напрямую к данным другого сервера, для прямого изменения и анализа реплицируемых данных с обеих концов, имея при этом гарантированный устойчивый канал связи (ADSL, выделенный канал, двупроводная линия Dial-Up и пр.), то такой вид синхронизации назовем прямым. Если же канал неустойчивый и не гарантирует устойчивую связь без падений во время процесса синхронизации и данные приходится передавать цельными пачками, при этом принимающая сторона во время закачки и анализа данных не имеет немедленной возможности опросить источник при возникновении на ее взгляд сомнительных моментов, а решение "Что делать?" принимать в любом случае нужно, то такой вид синхронизации будем называть недетерминированной или вероятностной.
  IV. По способу анализа реплицируемой информации. Если ядро алгоритма работает по принципу сравнения записей одной таблицы с записями другой, и на основании этого принимается решение о синхронизации, то такой процесс будем называть репликацией по текущему состоянию. Если в базе предусмотрен журнал вносимых изменений в БД, и алгоритм репликации переносит измененя по дельтам изменений накопленным в журнале, то такой процесс назовем дельта репликацией.


     Как видно из вышеприведенного списка, вариантов репликации существует довольно большое количество и подробно изучать все из них может не хватить здоровья. Поэтому, кратко остановимся на нескольких вариантах, а затем я подробно изложу разработанный мной алгоритм репликации данных на примере одного из своих проектов распределенной БД.
     Итак мы разбили репликацию на однонаправленную и мультинаправленную. На первый взгляд может показаться, что однонаправленная синхронизация сама по себе достаточно тривиальна, поскольку с точки зрения трудозатрат на кодинг алгоритма - вне конкуренции. Просто берем базу источник, смотрим что там нового умудрились наделать пользователи и добавляем изменения в базу приемник. Однако не все так радужно, как это кажется. Проблемы мгновенно возникают, когда необходимо несколько баз источников синхронизировать с одной базой приемником. Исходя из своего личного опыта разработки и эксплуатации БД могу сказать, что самый ужасный бичь практически всех баз это раздвоение элементов справочников, которое создается некоторыми очень продвинутыми в умственном отношении пользователями. Я уверен, что любой администратор БД, который читает эту статью, со мной согласится. Теперь представьте себе, что существует некоторая центральная БД (ЦБД) и несколько удаленных БД (УБД). На всех УБД пользователи вносят информацию независимо друг от друга и, соответственно, независимо растут справочники; при этом синхронизация работает в отложенном режиме, т..е УБД1 добавила, например, ЭЛЕМЕНТ1 в справочник клиентов, и через некоторое время УБД2 решила также провести опрацию связанную с ЭЛЕМЕНТ1. Но ЭЛЕМЕНТ1 в УБД2 еще пока не существует по причине отложенной синхронизации, поэтому оператор УБД2 добавляет ЭЛЕМЕНТ1 в справочник клиентов. На самом деле, оператор УБД2 в этом конкретном случае ни в чем не виноват, просто так само сабой произошло в силу незвисящих от оператора причин задержки синхронизации. Но факт остается фактом. В такой ситуации проблема с задвоением элементов справочников может довести до истерики любого БД администратора. Рекорд, который поставили мои юзера в базе с подобной организацией при трех УБД, составил до 8 контор ООО "АвтоРусь" в неделю. Они сумели перебрать практически все варианты с пробелами, типом собственности, заглавными, происными буквами, с дефисами и кавычками в придачу.
     Теперь обратимся к репликации мультинаправленной. В этом случае обмен данными идет в оба конца, но при этом проблема облегчется только при репликации реального времени. А при отложенной синхронизации проблема останется и добавится еще одна проблема - исправления и удаления документов БД задним числом. Представьте ситуацию, УБД1 содержит документ, что КЛИЕНТ1 принес в кассу 1000р. Произошла репликация. ЦБД приняла этот документ и отослала его УБД2. В результате УБД1, УБД2 и ЦБ содержит документ об оплате КЛИЕНТОМ1 1000р в кассу предприятия. Через несколько часов выяснилось, что КЛИЕНТ1 в справочниках задвоен и 1000р поставили не на ту запись справочника клиентов. Оператор на ЦБД и УБД2 выяснили это независимо друг от друга и ни слова ни говоря друг другу взяли и переставили этот документ на правильный элемент КЛИЕНТ1. В это же самое время УБД2 видит, что клиент оплатил 1000р и отпускает товар в счет этой оплаты на элемент КЛИЕНТ1. В момент синхронизации произойдет следующее. Во-первых; если алгоритм сделан достаточно умным, то одновременное изменение сделаное УБД1 и ЦБД в момент репликации будет отработано ЦБД корректно без вмешательства оператора для разрешения конфликта какой из двух документов считать правильным. Далее в момент связи ЦБД с УБД2 надо помнить, что УБД2 харинит неверную копию документа об оплате. То есть ЦБД надо в каждый момент времени знать состояние сонхронизации своих данных относительно каждой из УБД (А теперь представьте, что УБД могут возникать новые в неограниченном количестве по желанию вашего руководства. Из своего опыта могу сказать, что за два года работы имея постоянно три УБД, мое руководство умудрялось открывать новые временно работающие около месяца центры учета с новой УБД около 20 раз). Но предположим, что алгоритм поддерживает разрешение и этого конфликта. После синхронизации ЦБД переправляет документ об оплате в УБД2 на правильного клиента. Вроде бы все так и должно быть, однако документ об отпуске товара в УБД2 так и останется на неправильном клиенте. То есть, логика следования документов нарушена. Боротся с такими логическими ошибками крайне тяжело. И тем не менее при разработке очередного проекта, необходимо так разрабатывать алгоритм синхронизации, чтобы минимизировать вред от подобных действий пользователей.
     С точки зрения репликации реального времени, особых проблем у алгоритма синхронизации возникать не должно, поскольку такая репликация предпологает наличие постоянного надежного канала связи. И все изменения данных довольно быстро проходят по всем УБД, тем самым снижая риск возникновения логических несостыковок данных, вызванных вносимыми изменениями задним числом. Однако при наличии постоянного, хотя и медленного, канала связи, возможно, есть смысл не городить репликацию, а постараться написать такого клиента, который за приемлемое время, будет осуществлять подключение и работу непосредственно с ЦБД, и таким образом отказаться от самого процесса репликации. Конечно в каждом конкретном случае принятие решения за разработчиком.
     Теперь обратимся к прямой и недетерминированной репликации. При разработке очередного проекта БД обязательно необходимо провести тест канала связи со всеми планируемыми УБД. Поскольку на своем опыте столкнулся с ситуацией, когда с двумя из трех УБД модемная связь устойчивая, а с третей УБД соединение держится плохо и часто падает, не передав и сотни килобайт. В такой ситуации, если разработчик выберет режим прямой репликации, риск получить базу с кучей глюков очень велик. Затраты времени на их устранение будут прсто несоизмеримы с количеством времени жизни отпущенного природой на одного человека в России. Поэтому, если канал связи неустойчивый надо попытаться родить алгоритм недетерминированной синхронизации, который в любой момент времени после разрыва связи знает, как продолжить репликацию после восстановления соединения, даже если между сеансами репликации сумел вклинится сеанс репликации с какой-то другой УБД. Некоторые могут сказать, что я уж слишком сильно фантазирую на тему возможных ситуаций. Однако, я просто делюсь своим трехлетним опытом эксплуатации распределенной БД. И все примеры, приведенные мной, абсолютно реальны - лучше быть готовым к ним за ранее, чем решать их сходу. Это значительно продлит жизнь администратору БД, который будет рулить распределенной БД, если вы внезапно захотите поменять место работы. Поэтому, господа разработчики, думайте о тех, кто будет администрить вашы БД после вас.
     Рассматривая классификацию процесса репликации с точки зрения алгоритма анализа механизма репликации, то здесь вроде бы все и так интуитивно понятно. Стоит только упомянуть, что не стоит устраивать репликацию по текущему состоянию в чистом виде. Надо все-таки принять меры, для того, чтобы из кучи записей БД, работать только с новыми, измененными и удаленными, а не шарить по всем записям без исключения. Это, надеюсь, понятно без дополнительных пояснений.
     До сих пор, как бы неявно я подразумевал, что распределенная структура имеет как бы один главный центр учета и много периферийных. То есть, в нашей терминологии ЦБД и УБД1, УБД2 ..... И взаимоотношения, с точки зрения репликации, я строил по принципу ЦБД - УБД1, ЦБД - УБД2, .... Соответственно и примеры конфликтов, причем далеко не всех, были описаны исходя из этой ситуации. Однако, опять же, обратимся к моему опыту эксплуатации. Представьте, вызывает вас к себе руководство и говорит, что мол ЦБД - УБД это хорошо, но через неделю мы будем перемещать товар с УБД1 на УБД2, минуя ЦБД, поскольку ЦБД находится в офисе, который по воскресеньям не работает, а перемещать товар мы будем и по субботам и по воскресеньям. В этой ситуации есть два решения: во-первых, можно ничего не делать и на добровольных началах выходить на работу по субботам и воскресеньям, чтобы самому контролировать правильность процесса репликации от УБД1 на УБД2 через ЦБД; во-вторых, попытаться подумать, а чем собственно прямая связь УБД1 - УБД2 нам грозит и в третьих, написать заявление об уходе и пойти пить пиво. Если вы в этой ситуации выбрали второе решение, то могу сказать только одно - лучше бы вы выбрали третье. Не вдаваясь в тонкости данной ситуации приведу только два примера (подчеркиваю только два из пары десятков с которыми вы столкнетесь на практике).
     Пример 1. В справочнике номенклатуры существует ТОВАР1. На УБД1 ТОВАР1 есть в остатках в кол-ве 100 штук. На УБД2 этого товара в остатках 0 шт. По данным центральной базы этого товара в остатках 100 шт и весь он на УБД1. Товар в количестве этих 100 шт перемещают на УБД2, используя соединение УБД1 - УБД2. Затем УБД2 выписала клиенту расход на все 100шт. Тут внезапно оператор ЦБД находит ошибку, что оказывается этот товар заприходовали неправильно и его надо разбить на две части - 20 шт на свою контору и 80 штук на ЧП Иванов, который освобожден от НДС, поскольку зарегистрирован на оффшорной территории земли им. Франца-Иосифа. Ну продвинутый оператор ЦБД убивает первоначальную накладную и делает две новых. Причем, по правилам, установленным начальством, ТОВАР1 для ЧП должен сразу приходоваться на УБД2, ну оператор и заприходовал 80 штук на УБД2 а 20 штук на УБД1. И с чувством гордости за хорошо выполненное дело пошел пить кофе.
     Пример 2. УБД1 в воскресенье получила НОВЫЙТОВАР1. Создала новый элемент в справочнике номенклатуры и заприходовала его. В то же самое время УБД2 также получила НОВЫЙТОВАР1. Создала свой элемент в справочнике номенклатуры и заприходовала его. Звонит начальство на УБД1 и говорит, чтобы УБД1 переместила 100 шт НОВЫЙТОВАР1 на УБД2. Ну УБД1 соединяется с УБД2 и передает документ о перемещении. Дальнейшие последствия таких действий предсказуемы еще меньше чем обезьяна с гранатой и обуславливаются только мудростью алгоритма репликации и фантазией операторов на тему сколькими способами, варируя пробелы, верхний и нижний регистр, наличие одинарных или двойных кавычек, перестановку букв при этом перемежая все это орфографическими ошибками можно набрать слово НОВЫЙТОВАР1. Не раздувая более данным примером статью, скажу лишь, что если в понедельник после глобальной синхронизации вы обнаружите в номенклатуре две одинаковых записи НОВЫЙТОВАР1, то это значит, что ваш алгоритм синхронизации по крайней мере работает удовлетворительно. От себя добавлю, что если у вас есть стандартный репликатор, позволяющий смоделировать подобную ситуацию, то, поставив аналогичный эксперимент, вы с удивлением обнаружите в ЦБД четыре записи НОВЫЙТОВАР1.
     Ну вот, господа, я изложил два, с позволения сказать, совсем незатейливых примера из своей жизни. Выводы делайте сами по поводу связи УБД - УБД.

     Подводя итог вышесказанному, хочу сделать некоторые выводы.
     Прежде всего, распределенная БД (РБД) характеризуется наличием естественных конфликтных ситуаций, вытекающих из ситуации самостоятельной, независимой жизни УБД друг от друга. Возникающие конфликты могут разрешаться либо на уровне алгоритма репликации, использующего правила административного старшенства (Я - ЦБД, значит я права),либо, руководствуясь поздним по времени событием (кто последний, тот и прав), либо на уровне вмешательства ответственного оператора. От себя добавлю, что баланс между количеством разрешаемых и неразрешаемых конфликтов распределяется целиком между совестью разработчика, финансовыми возможностями заказчика и ленью конкретного программиста ответственного за разработку алгоритмов синхронизации распределенного проекта, хотя даже при избытке первых двух факторов и полного невмешательства третьего, все равно невозможно написать алгоритм репликации, разрешающий все конфликты без участия ответственного оператора или администратора БД, причем не последнюю лепту в эту ситуацию вносит, порой, извращенная технология ведения финансового хозяйства заказчика, обусловленная необходимостью отмазаться от налогов на прибыль.




4. Проблема синхронизации уникальных идентификаторов


     Не сильно ошибусь, если скажу, что основа любой учетно-финансовой автоматизированной системы - объекты хранящиеся в справочниках БД. А для справочника главное - это поддержание уникальности своих ID-идентификаторов, как минимум. Только в этом случае эту таблицу можно называть справочником. Обычно ID записи создается хранимым генератором уникальной последовательности целых чисел, и при помощи триггера BeforeInsert производится вставка новой записи в справочник. А теперь попытайтесь перенести все сказанное на распределенную базу, в которой ЦБД и все УБД работают независимо друг от друга, и, соответственно, должны уметь генерировать уникальные ID своих справочников, не консультируясь друг с другом. Для тех, кто еще пока не въехал в проблему, я позволю себе процетировать Игоря Трофимова:

     Для унификации желательно, чтобы в каждой таблице, включенной в процесс синхронизации был суррогатный первичный ключ (ID) - обычно INTEGER. Впрочем, чаще всего так и есть. Как же мы можем обеспечить уникальную идентификацию записей.

  1. Можно ввести в каждую таблицу дополнительное поле - номер БД, в которой эта запись была создана впервые (DBID). При этом, очевидно, ID уже не будет являться первичным ключем, вместо этого первичным ключем будет пара (DBID, ID). Следует заметить, что из-за этого данное решение не слишком привлекательно.
  2. Можно сделать первичным ключем строку спецального формата, например XXXX-YYYY-ZZZZZZZZ, где XXXX - это идентификатор базы данных, где запись была создана впервые, YYYY - идентификатор таблицы, ZZZZZZZZ - идентификатор записи внутри конкретной таблицы конкретной БД. Такое решение является хорошо масштабируемым, позволяет "запихать" в ID много дополнительной информации, но у него тоже есть минусы. Во-первых, некоторая избыточность информации. Во-вторых, более сложный механизм генерации таких ID. И еще, неплохо было-бы ограничить возможные значения ID данным форматом. Это тоже заботы.
  3. На мой взгляд, для InterBase лучшим вариантом является следующий - ID для всех таблиц генерируется обычным триггером, выбирающим значения из генератора. При этом начальное значение генератора различно для разных БД, за счет чего обеспечится уникальность ID по всем БД. Данный подход характерен для InterBase. Применение его для других СУБД может быть ограничено невозможностью задать начальное значение для счетчика автоинкрементных полей.


     Здесь я обсолютно со всем согласен. Хочу лишь добавить, что выбрав второй путь мы будем строить первичный ключ типа CHAR. А это значит, что производительность БД будет падать пропорционально накоплению количества записей в таблицах, так как процессор сравнивает INTEGER vs INTEGER во много раз быстрее, чем CHAR vs CHAR. Надеюсь, дальнейшие разъяснения не требуются.
     В дальнейшем вы увидите, что существует еще и четвертый путь, управления уникальными значениями в расспределенной базе.




5. Пример решения задачи репликации справочников распределенной базы


     Задача, которую мне пришлось решать, состояла в следующем.
     Представьте себе контору. занимающуюся торговлей автомобилями. Есть центральный офис (ЦО), кторый хочет получать ежедневные данные о продажах и оперативные остатки по автомобилям. Сам ЦО автомобилями не торгует. Непосредственная торговля ведется на стоянках, расположенных в разных концах города, а иногда и в другом городе. В конце каждого месяца надо делать месячные отчеты. Прямой проводной связи между ЦО и стоянками не существует. Максимум на что можно расчитывать - это телефонная связь.
     Проанализировав исходные данные, и протестировав телефонные каналы, я пришел к следующему выводу. Синхронизация базы должна производиться, только между ЦБД и УБД. Связи типа УБД - УБД надо исключить, чтобы не усложнять алгоритм. Однако стоянки должны перемещать автомобили друг между другом, не прибегая к помощи оператора ЦБД. Поэтому на серваке InterBase ЦБД надо создать сервисную службу со слушающим сокетом TCP/IP, к которому будут подключаться стоянки по модему и синхронизировать свои данные. Таким образом, выбрали мильтинаправленную отложенную репликацию. Однонаправленная здесь не катит, поскольку данные могут меняться как с удаленных стоянок (УС), так и из ЦО. Постоянно связь держать нет возможности, поэтому в реальном времени организовать репликацию также не удастся. Теперь необходимо перейти к тестированию канала и выбору способа передачи данных. В ЦО ADSL канал в интернет, поблем с трафиком не существует. Однако, тесты соединений с городскими провайдерами паказали, что на двух стоянках соединения довольно устойчивы, а на одной модемная связь постоянно падает из-за сильных помех в канале. При этом резко наростали затраты на передачу информации, если подключаться через независимого провайдера. Выход был найден такой. Поднимаем Dial-Up сервис на центральном серваке, к которому стоянки будут коннектиться по модему. В этом случае затраты только на местные телефонные переговоры. Для обмена информацией необходимо разработать, что-то, типа, протокола обмена данными между ЦБД и УБД. В результате схему учетной системы можно представить в таком виде:

     Теперь обратимся к проблеме генерации уникальных ID для справочников. Очевидно, что из трех вышепредложенных вариантов самым реальным является третий, основанный на смещении диапазона INTEGER для генераторов ID-значений. Однако, по условию задачи, количество УБД ничем не ограничивается. Значение INTEGER это 4 байта. Три байта можно использовать для генерации в ЦБД и со сомещением на 1 в четвертом байте можно использовать для УБД. Но в таком случае УБД может быть только 255. Это - нехорошо. Стоянки могут плодиться и закрываться в неограниченном количестве, следовательно, 255 это для нас мало. В этой ситуации можно пойти другим путем, который я подсмотрел в FoxPro программе by Олег Космачев, обслуживавшей эту контору ранее. Этот метод был мною доработан и устранены слабые места, но основная идея осталась прежняя. Метод заключается в следующем.
      Во-первых, все генераторы ЦБД инициализируем с нуля. Во-вторых, все генераторы УБД инициализируем с какого-то дальнего значения, но одинакого для всех УБД. То есть ЦБД инициализируем с 0, а каждую новую УБД с 3 500 000 000. Цифра достаточно большая, поэтому мы гарантированы практически на 100%, что ЦБД никогда не доползет до начального значения на УБД. В-третьих, как вы можете заметить, осталась проблема уникальности УБД - УБД. Это решается следующим образом. Обратите внимание, что уникальные значения, вырабатываемые ЦБД, являются также уникальными для всей системы в целом, причем, как было показано ранее, промежуток между начальными значениями для ЦБД и УБД настолько велик, что не стоит беспокоится об их пересечении. От сюда вывод - а что если для УБД использовать, те же значения, которые генерирует ЦБД. Оказывается можно и даже нужно. Делается это следующим образом. УБД для новых элементов заводит свои ID, начиная с 3 500 000 000. Во время сеанса синхронизации ЦБД генерирует свои и отсылает УБД новые номера. УБД принимает ответ и каскадом заменяет все свои локальные ID на глобальные ID, которые для нее сгенерировала ЦБД. Уникальность справочников у УБД в этом случае гарантирована, так как ее локальные ID и глобальные ID от ЦБД никогда не пересекаются. Когда другие УБД будут синхронизироваться с ЦБД, то ЦБД просто пришлет им новые элементы справочников с глобальными ID, которые она сгенерировала при получении от УБД-создателя новых элементов. Такм образом, после того, как все УБД проведут цикл репликации данных, новые элементы встанут в справочниках с глобальными ID, которые генерируются ЦБД. В этом и состоит причина, почему я для всех УБД предлагаю инициализировать генераторы с одним и тем же начальным значением. Итак проблема уникальности для неограниченного количества УБД при помощи переназначения локальным ID глобальных ID решается, как это было показано выше.
     Далее рассмотрим формат передачи данных в процессе синхронизации. Поскольку канал неустойчивый, то репликацию придется проводить по недетерминированному варианту. Это, в свою очередь, приводит нас к проблеме разработки такого алгоритма, который бы обеспечивал продолжение работы процесса репликации при потере связи и последующем ее восстановлении. Решение было найдено следующее. Протокол TCP/IP гарантирует доставку пакетов. Следовательно, пачка ушедшая к приемнику и подтвержденная на стороне отправителя доставлена без ошибок. Тогда минимальной единицей раплицируемых данных должен быть файл заранее определенной длинны, в котором заключена вся информация для работы алгоритма репликации на стороне приемника. Мной было принято решение, что БД на стороне отправителя должна проанализировать состояние своих данных по отношению к БД, с которой проводится сеанс репликации и подготовить, фактически, текстовый скрипт SQL, который принимающая сторона должна просто тупо шаг за шагом запускать на выполнение. Естественно это влечет за собой необходимость каждой БД знать состояние своих записей по отношению к БД, с которой она соединяется в момент сеанса репликации. Другими словами УБД, должна постоянно регистрировать состояние своих записей по отношению к ЦБД, а ЦБД должна знать состояние своих записей по отношению к каждой из УБД. С одной стороны, может показаться, что это нереальная задача поскольку никаких ограничений на количество УБД не существует, однако мной было найдено решение этой проблемы. То как это делается, будет продемонстрировано позже. А сейчас подробно рассмотрим дисциплину связи.
     На основании. предыдущего опыта обслуживания распределенной БД, мной была разработана следующая дисциплина связи. Алгоритм привожу по шагам:
  1. Ответственный клиент на УБД запускает пользовательский сервис синхронизации данных. Задает LOGIN и PASSWORD удаленного соединения и стартует сервис набора номера. После того, как модемы ЦБД и УБД засинхронизировали несущие, УБД пытается подключиться к слушающему сокету на 5000 порт, на котором должен быть поднят сервис репликации ЦБД.
  2. 5000 - порт сервиса принимает соединение и посылает на удаленного клиента запрос на получение LOGIN и PASSWORD. Одновременно с этим входит в режим отсылки команды ENGAGED (сервис занят) другим клиентам, попытавшимся подключиться на этот сервис, в то время как один клиент уже подключен. По плану толко одна УБД пожет синхронизировать свои данные в один и тот же момент времени.
  3. УБД принимает запрос от ЦБД и отправляет LOGIN и PASSWORD, заданные ответственным оператором УБД перед началом сеанса соединения.
  4. ЦБД сверяет принятый LOGIN и PASSWORD от УБД со своим списком. Если авторизация проходит успешно, то отсылает на УБД команду готовности READY и входит в режим ожидания команд от УБД. Если произошла ошибка авторизации, то на УБД отсылается команда отказа авторизации с сообщением причины и осуществляет разрыв связи, переходя в состояние прослушивания 5000 - ого порта и снимает состояние ENGAGED.
  5. УБД получает сигнал готовности от ЦБД. Запускает процедуру анализа своих данных и подготовки файла исполняемого SQL скрипта. Сжимает файл ZIP алгоритмом и выдает команду готовности предачи данных GET READY TO RECEIVE FILE, LENGTH: *******, сообщая ЦБД длину передаваемого файла данных. Затем входит в режим ожидания подтверждения готовности от сервиса ЦБД.
  6. На стороне ЦБД принимается команда подготовки приема данных, считывается длина предпологаемого файла. Затем УБД отсылается разрешение на передачу тела файла START TRANSMISSION и переход в состояние приема данных:
  7. УБД принимает приказ о передаче START TRANSMISSION и отправляет файл данных. После окончания передачи УБД переходит в режим ожидания приказов от ЦБД.
  8. ЦБД получает пакеты от УБД. Когда длина принятого файла совпадет с заявленой длиной от УБД, ЦБД запускает процедуру распаковки принятого файла, после чего последовательно считывает принятые SQL команды и запускает их в своей среде.
  9. После обработки принятых данных ЦБД запускает процедуру обработки своих данных в контексте текущего подключенного клиента УБД. Формирует небходимый файл SQL скриптов, запаковывает их ZIP алгоритмом и сохраняет во временном файле. Как только файл для отправки готов, ЦБД посылает УБД, которая находится в режиме ожидания приказов, команду готовности к приему файла, сообщая длину передаваемого файла данных в байтах: GET READY T RECIEVE FILE, LENGTH: *******. После этого переходит в режим ожидания подтверждения готовности к приему файла от УБД.
  10. УБД принимает команду GET READY TO RECEIVE FILE, LENGTH: *******, запоминает длину ожидаемого файла и подтверждает готовность принять данные, отсылая на УБД команду START STRANSMISSION.
  11. ЦБД принимает от УБД START STRANSMISSION и отсылает подготовленный файл с данными на УБД. После этого падает в состояние ожидания ответной реакции от УБД.
  12. УБД принимает файл от ЦБД. Распаковывает его и запускает SQL скрипты, переданные ЦБД. Потом запускает процедуру анализа своих данных и подготовки файла исполняемого SQL скрипта на основании принятых данных от ЦБД. Сжимает файл ZIP алгоритмом и выдает команду готовности предачи данных GET READY TO RECEIVE FILE, LENGTH: *******, сообщая ЦБД длину передаваемого файла данных. Затем входит в режим ожидания подтверждения готовности от сервиса ЦБД.
  13. ЦБД повторяет шаг 6.
  14. УБД повторяет шаг 7.
  15. ЦБД повторяет шаг 8.
  16. После успешного выполнения всех команд УБД отсылает на УБД команду завершения сеанса синхронизации SHUT DOWN CONNECTION. PROCESS ENDED SUCCESSFULLY и снимает ENGAGED c сокета 5000, так, что другие УБД могут коннектится и запустить наконец свои сеансы репликации.
  17. На УБД, тем временем, приходит команда завершения сеанса репликации и УБД разрывает модемное соединение.
  18. Сеанс репликации с обоих концов завершен.


     Итак дисциплину связи мы рассмотрели. Теперь самое время перейти к принципам реализации справочников внутри БД. Для примера рассмотрим несколько таблиц. Для нашего примера это будут два абстрактных справочника и карта центров учета со следующей структурой:

/* Справочник моделей автомобилей */

CREATE TABLE MODELS (
MODID INTEGER NOT NULL, /* ID модели автомобиля, генерируется генератором и присваевается тригером BeforeInsert */
NAME VARCHAR(20) NOT NULL, /* Наименование модели. Задается оператором Пример: ГАЗ 3110 */
DELMARKED SMALLINT, /* признак пометки записи на удаление 1 - помечена на удаление, 0 - нормальное состояние */
PARAM INTEGER, /* параметр для передачи в триггера различных значений */
OLDCENTER INTEGER, /* Параметр для передачи в триггер ID центра учета УБД*/
OLDID INTEGER); /* Параметр для передачи в триггер ID записи, поражденного УБД*/

/* Справочник автомобилей */

CREATE TABLE CARS (
CARID INTEGER NOT NULL, /* ID автомобиля, генерируется генератором и присваевается тригером BeforeInsert */
MODEL INTEGER NOT NULL, /* Cылка из справочника моделей*/
VIN VARCHAR(20), /* VIN автомобиля. Это только для примера, чтобы уж совсем не делать таблицу пустой*/
DELMARKED SMALLINT,
PARAM INTEGER,
OLDCENTER INTEGER,
OLDID INTEGER);

/* Foreign keys definition */

ALTER TABLE CARS ADD CONSTRAINT CARS_MODEL FOREIGN KEY (MODEL) REFERENCES MODELS (MODID) ON UPDATE CASCADE;

/* Справочник центров учета, к которым приписана ЦБД и все УБД */

CREATE TABLE CENTERS (
CENTERID INTEGER NOT NULL, /* ID центра учета, генерируется генератором и присваевается тригером BeforeInsert */
NAME VARCHAR(20) NOT NULL, /* наименование центра учета */
VIRTUAL SMALLINT NOT NULL, /* 1 - если этот центр учета виртуальный, 0 - если реальный */
VISIBLE SMALLINT NOT NULL, /* 1 - если этот центр видимый из текущей базы, 0 - невидимий в текущей базе */
MAINCENTER SMALLINT); /* 1 - если это ЦБД, 0 - если УБД */


     В таблицах я убрал все, не относящиеся к делу, поля и оставил, толко те, которые нам понадобятся для понимания работы алгоритма. Тем не менее хочу сразу дать несколько комментариев.
     В таблице CENTERS есть поля VIRTUAL и VISIBLE. По условию задачи, в одной УБД могло существовать несколько центров учета, причем все эти центры учета проводили операции физически в одной и той же БД. Поэтоиу, была необходимость как-то отделять такие центры учета друг от друга. Для этого служит поле VIRTUAL. Разница для виртуальным и реальным ЦУ состоит в тои, что виртуальный ЦУ не имеет полномочий на репликацию, за него это делает реальный ЦУ в чьей видимости (VISIBLE) он находится. Для тех, кто не понял, в двух словах это означает, что чтобы из триггера или хранимой процедуры нам определить ID ЦУ, в котором мы сейчас находимся то надо выполнить:
select centerid from centers where visible=1 and virtual=0
     Итак у нас есть три справочника: CARS, MODELS и CENTERS. Для упрощения описания алгоритма репликации будем работать только со связанными аблицами CARS и MODELS, а таблицу CENTERS будем считать нереплицируемой - она не изменяется пользователями и для каждого центра учета настроена правильно. Пусть CENTERS в ЦБД содержит следующие данные:

CENTERID NAME VIRTUAL VISIBLE MAINCENTER PARAM
1 Центральный офис 0 1 1 0
2 Удаленная стоянка 0 0 0 0

а в УБД она имеет следующее содержание:

CENTERID NAME VIRTUAL VISIBLE MAINCENTER PARAM
1 Центральный офис 0 0 1 0
2 Удаленная стоянка 0 1 0 0

Таким образом, имеем две стоянки: Центральный Офис (1) и Удаленная стоянка (2). Предположим также, что таблицы CARS и MODELS на данном этапе не содержат никаких данных.
     Следующим этапом нам необходимо разработать механизм учета вносимых пользователями изменений в данные. Это будем делать следующим образом. Для каждой БД заведем таблицу с такой структурой:

/* Журнал удаленного састояния объектов */

CREATE TABLE REMOTE (
N INTEGER NOT NULL, /* ID записи, генерируется генератором и присваевается тригером BeforeInsert */
CENTER INTEGER NOT NULL, /* ID центра учета для которого сделана данная запись */
CARS INTEGER, /* ID записи справочника CARS */
CARSSTATE CHAR(1), /* состояние записи из CARS в операции репликации*/
CARSACT VARCHAR(1), /* признак произведеного действия юзером над записью из CARS*/
CARSOLD INTEGER, /*ID записи из CARS, которой был сгенерирован УБД*/
MODELS INTEGER, /* ID записи справочникаMODELS */
MODELSSTATE CHAR(1), /* состояние записи из MODELS в операции репликации*/
MODELSACT VARCHAR(1), /* признак произведеного действия юзером над записью из MODELS*/
MODELSOLD INTEGER); /*ID записи из MODELS, которой был сгенерирован УБД*/

     Наименование полей этой таблицы были выбраны таким образом, чтобы при необходимости можно было бы осуществить автоматизированную обработку и построение SQL скриптов. Как видно из названий, состояние каждого справочника представлены четырьмя полями. Кроме этого есть еще два служебных поля N и CENTER. Поле N несет на себе функцию уникального ключа, а поле CENTER отождествляет состояние записей остальных информационных полей по отношению к ID ЦУ, хранящемуся в этом поле. Информационное поле с именем таблицы хранит в себе ссылку на информационный объект конкретного справочника. Поля с постфиксом STATE показывают уровень засинхронизированности записи этого справочника (фраза несколько корявая, но довольно точно отражающая происходящее). Постфикс ACT показывает какое действие было произведено с записью справочника оператором. Поля с постфиксом OLD несут достаточно специфическую нагрузку, обусловленную наличием одного узкого места в дисциплине репликации. Дело в том, что канал неустойчивый и обрыв связи в момент репликации - обычное дело. Поэтому может возникнуть критическая ситуация, когда УБД подключилась к ЦБД, передала ей первый скрипт SQL на выполнение и отвалилась. К этому времени ЦБД этот скрипт уже выполнила. Предположим, что в скрипте, пришедшем от УБД , были команды на создание новых элементов в справочнике. Согласно нашему алгоритму, ЦБД добавила эти элементы в свою базу, присвоила им глобальные ID. Однако на этом наша связь прервалась. В этом случае ситуация в БД на грани легкой кривизны, поскольку очевидно, что УБД дозвонится до ЦБД вновь и повторит создане этих записей, так как подтверждения об успешном добавлнии этих записей в ЦБД во время первого сеанса не было. Оставив такую ситуацию на самотек, мы не нарушим целостности БД, но породим два одинаковых объекта во всей БД. Для разрешения этого технологического конфликта как раз и служит поле с постфиксом OLD, на основании которого можно принять решение о том, повторное ли это добавление одного и того же объекта в справочник после падения канала, или свежее. Как это делается, мы увидим ниже.
     На основании предложенной структуры таблицы REMOTE можно видеть, что эта таблица может хранить в себе состояние обо всех записях всех существующих записей праллельно. Максимальный размер этой таблицы равен длинне самой длинной таблицы помноженной на количество физицеских УБД или удаленных центров учета (УЦУ). Сколько бы новых ЦУ ни появлялось, изменять структуру этой таблицы нет необходимости. Следует заметить, что только ЦБД должна хранить состояние своих записей по отношению ко всем УБД. Для УБД в этом необходимости нет. Поскольку мы не предпологаем отношений УБД - УБД, то в этой таблице поле CENTER всегда будет равно ID ЦБД. Хотя это и так, но не стоит исключать это поле из таблицы УБД, поскольку нет никакой гарантии, что в будущем не придется перейти к отношениям УБД - УБД. А наличие этого поля содержит в себе, практически, готовый механизм перехода к более сложным взаимоотношениям, не исключая и трехступенчатые (такие отношеня вообще тема для диссертаций, поэтому дискуссию на эту тему я развивать не буду).
     Еще одна таблица, которая нам понадобится для работы алгоритма следующая:

/* Таблица подчиненности справочников */

CREATE TABLE RELATIONS(
N INTEGER NOT NULL, /* ID записи, генерируется генератором и присваевается тригером BeforeInsert */
MASTER VARCHAR(15) NOT NULL, /* наименование master-таблицы */
MASTER_FIELD VARCHAR(15) NOT NULL, /* наименование поле master-таблицы */
SUBMIT VARCHAR(15) NOT NULL, /* наименование подчиненной таблицы*/
SUBMIT_FIELD VARCHAR(15) NOT NULL); /* наименование поля подчиненной таблицы*/

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

N MASTER MASTER_FIELD SUBMIT SUBMIT_FIELD
1 MODELS MODID CARS MODEL

     В реальной ситуации эта таблица должна быть заполнена вами в соответствии с вашей структурой данных и подчиненностью реплицируемых таблиц.
     Теперь рассмотрим формат информационного файла передачи данных. Как было упомянуто ранее, самое оптимальное решение этого вопроса, состоит в том, чтобы передавать готовые к исполнению SQL скрипты. Однако, желательно найти такое решение, чтобы иметь простой вариант не только формирования текста, но и его чтения и анализа тела файла на принимающей стороне. Мной было принято решение формировать этот файл в формате ini-файлов windows, в котором есть секции и ключи. Классы и функции для работы с такими файлами хорошо разработаны практически в любом нормальном языке. Таким образом, получаем гибкость текстового файла, который можно проанализировать визуально, если это будет необходимо, плюс легкость формирования и анализа содержимого. Кроме того, текст хорошо жмется zip алгоритмом, библиотеки которого общедоступны.
     INI-фал содержит следующие стандартные секции:

[HEADER]                                                // секция заголовка 
FileHeader=Information Exchange INI Protocol v 1.0 // тип протокола UserName=sysdba // login оператора осуществляющего репликацию TimeStamp=20.04.2001 16:31:57 //время формирования файла PackageId=132 // ID пакета обмена Source=2 // ID ЦУ, сформировавшего пачку Destination=1 // ID ЦУ для которого эта пачка предназначена
[NEW_RECORDS] // секция скриптов для вставки новых элементов справочников
[NEW_RECORDS_CONFIRMED] // секция подтверждения вставленных записей
[MODIFIED_RECORDS] // секция передачи модифицированных записей
[MODIFIED_RECORDS_CONFIRMED] // секция подтверждения приема модифицированных данных

     Предложенный вариант носит условный характер и в реальных пиложениях , скорее всего, будет изменен и дополнен. Однако данного варианта вполне достаточно для демонстрации разработанного мной алгоритма репликации. Из названий секций интуитивно понятно, какого рода записи они должны содержать. Секция HEADER несет в себе дополнительную информационную защиту от несанкционированного доступа и возможных предупреждений повторных обработок одного и того же файла. Остальные секции мы рассмотрим ниже.
     Для успешной работы алгоритма нам понадобится добавить в нашу базу пару триггеров на события вставки и редакторования, которые обеспечат нам сопровождение таблицы REMOTE. Ниже я привожу реально работающие в триггера от MODELS и CARS на события AfterInsert и AfterUpdate.

CREATE TRIGGER INSERT_MODEL1 FOR MODELS
ACTIVE AFTER INSERT POSITION 0
as
declare variable min_pos integer;
declare variable our_center integer;
declare variable main integer;
declare variable current_cnt integer;
declare variable model_id integer;
declare variable my_param integer;
declare variable main_cnt integer;
begin
  our_center=0;
  main=0;
  model_id = new.modid;
  /* считываем параметр, переданый в триггер*/
  my_param=0;
  if (new.param>0) then my_param = new.param;
  /* определяем параметры текущего центра учета */
  select centerid, maincenter from centers where visible=1
                                             and working=1
                                            into :our_center, :main;
  /* если нет текущего видимого центра учета, то это ошибка
     в структуре базы. Об этом надо сообщить */
  if ((our_center=0) or (our_center is NULL)) then exception no_vis_center;
  main_cnt=our_center;
  if (main<>1) then select centerid from centers where maincenter=1 into :main_cnt;
  if ((main_cnt=0) or (main_cnt is NULL)) then exception no_main_center;
  /* Если это главный центр учета, то устанавливаем флаг обновления на все невиртуальные
     центры учета */
  if (main=1) then begin
   if (my_param>0) then begin  /* Это означает что инициатива по добавлению новой модели
                  исходит от УБ с кодом my_param*/
    /* организуем цикл прохода по центрам учета */
    for select centerid from centers where working = 1     /* работающий */
                                     and virtual <>1       /* невиртуальный */
                                     and centerid <> :our_center  /* не равен главному */
                                     and centerid <> :my_param /* для удаленного ЦУ своя обработка */
                                     into :current_cnt
                                     do begin
      /* определяем ближайшую свободную строку для пометки изменения */
      min_pos=0;
      select MIN(N) from remote where center = :current_cnt  /* здесь определяем позицию */
                                and models = :model_id       /* для записи флага обновления */
                                into  :min_pos;
      if (min_pos>0) then   /* если для этого эл-та справочника существует запись в remote */
         update remote set modelsact="A", modelsstate="B" where n = :min_pos;
      else begin   /* Если нет записи в remote то ищем первую свободную */
        select MIN(N) from remote where center = :current_cnt
                                  and models is null
                                  into :min_pos;
        if (min_pos>0) then
          /* для найденой записи делаем update */
          update remote set models = :model_id, modelsact="A", modelsstate="B" where N = :min_pos;
        else
          /* если записи нет, то добавим новую */
          insert into remote(center,models,modelsact,modelsstate)
               values(:current_cnt, :model_id,"A","B");
      end
    end
    /* здесь производим обработку для УЦУ, который и был инициатором добавления нового
       элемента в ЦБ */

     /* определяем ближайшую свободную строку для пометки изменения */

    min_pos=0;
    select MIN(N) from remote where center = :my_param  /* здесь определяем позицию */
                              and models = :model_id       /* для записи флага обновления */
                              into  :min_pos;
    if (min_pos>0) then   /* если для этого эл-та справочника существует запись в remote */
       update remote set modelsact="I", modelsstate="A", modelsold=new.oldid
                     where n = :min_pos;
    else begin   /* Если нет записи в remote то ищем первую свободную */
      select MIN(N) from remote where center = :my_param
                                and models is null
                                into :min_pos;
      if (min_pos>0) then
        /* для найденой записи делаем update */
        update remote set models = :model_id,
                          modelsact="I",modelsstate="A", modelsold=new.oldid
                      where N = :min_pos;
      else
        /* если записи нет, то добавим новую */
        insert into remote(center,models,modelsact,modelsstate,modelsold)
             values(:my_param, :model_id,"I","A",new.oldid);
    end
   end
   else begin /* Это в том случае, если  никакого параметра передано не было
                 и добавление нового элемента происходит по инициативе ЦБ*/
    /* организуем цикл прохода по центрам учета */
    for select centerid from centers where working = 1     /* работающий */
                                     and virtual <>1       /* невиртуальный */
                                     and centerid <> :our_center  /* не равен главному */
                                     into :current_cnt
                                     do begin
      /* определяем ближайшую свободную строку для пометки изменения */
      min_pos=0;
      select MIN(N) from remote where center = :current_cnt  /* здесь определяем позицию */
                                and models = :model_id       /* для записи флага обновления */
                                into  :min_pos;
      if (min_pos>0) then   /* если для этого эл-та справочника существует запись в remote */
         update remote set modelsact="A", modelsstate="B" where n = :min_pos;
      else begin   /* Если нет записи в remote то ищем первую свободную */
        select MIN(N) from remote where center = :current_cnt
                                  and models is null
                                  into :min_pos;
        if (min_pos>0) then
          /* для найденой записи делаем update */
          update remote set models = :model_id, modelsact="A", modelsstate="B" where N = :min_pos;
        else
          /* если записи нет, то добавим новую */
          insert into remote(center,models,modelsact,modelsstate)
              values(:current_cnt, :model_id,"A","B");
      end
    end
   end
  end else begin
  /* здесь алгоритм примерно такой же, только работаем с одним (главным) центром учета */

   if ((my_param>0) and (my_param=main_cnt)) then begin /* Это вариант,
                   когда ЦБ прислала новый элемент */
    min_pos=0;
    select MIN(N) from remote where center = :main_cnt
                              and models = :model_id
                              into  :min_pos;
    if (min_pos>0) then
       update remote set modelsact="I",modelsstate="A" where N = :min_pos;
    else begin
      select MIN(N) from remote where center = :main_cnt and models is null
                                into :min_pos;
      if (min_pos>0) then
        update remote set models = :model_id, modelsact="I", modelsstate="A"
                    where N = :min_pos;
      else
        insert into remote(center, models, modelsact, modelsstate)
               values(:main_cnt, :model_id,"I","A");
    end
   end
   else begin   /* Этот случай, когда УБ сама добавила новый элемент */
    min_pos=0;
    select MIN(N) from remote where center = :main_cnt
                              and models = :model_id
                              into  :min_pos;
    if (min_pos>0) then
       update remote set modelsact="A",modelsstate="B" where N = :min_pos;
    else begin
      select MIN(N) from remote where center = :main_cnt and models is null
                                into :min_pos;
      if (min_pos>0) then
        update remote set models = :model_id, modelsact="A",modelsstate="B"
                      where N = :min_pos;
      else
        insert into remote(center, models, modelsact, modelsstate) values(:main_cnt,
                                                                          :model_id,
                                                                          "A",
                                                                          "B");
    end
   end
  end
end

CREATE TRIGGER UPDATE_MODEL FOR MODELS ACTIVE AFTER UPDATE POSITION 0 as declare variable min_pos integer; declare variable our_center integer; declare variable main integer; declare variable current_cnt integer; declare variable main_cnt integer; declare variable model_id integer; declare variable my_param integer; begin /* Смысл этого триггре такой же как и у INSERT_MODEL1 */ if (new.modid=old.modid) then begin our_center=0; main=0; model_id = old.modid; if (new.modid>0) then model_id=new.modid; my_param=0; if (new.param>0) then my_param = new.param; select centerid, maincenter from centers where visible=1 into :our_center, :main; if ((our_center=0) or (our_center is NULL)) then exception no_vis_center; main_cnt=our_center; if (main<>1) then select centerid from centers where maincenter=1 into :main_cnt; if ((main_cnt=0) or (main_cnt is NULL)) then exception no_main_center; if (main=1) then begin if (my_param>0) then begin /* значит update пришел от УБ */ for select centerid from centers where working=1 and virtual<>1 and centerid <> :our_center and centerid <> :my_param into :current_cnt do begin min_pos=0; select MIN(N) from remote where center = :current_cnt and models = :model_id into :min_pos; if (min_pos>0) then update remote set modelsact="M",modelsstate="B" where N = :min_pos; else begin select MIN(N) from remote where center = :current_cnt and models is null into :min_pos; if (min_pos>0) then update remote set models = :model_id, modelsact="M",modelsstate="B" where N = :min_pos; else insert into remote(center, models, modelsact,modelsstate) values ( :current_cnt, :model_id, "M","B"); end end min_pos=0; select MIN(N) from remote where center = :my_param and models = :model_id into :min_pos; if (min_pos>0) then update remote set modelsact="E",modelsstate="M" where N = :min_pos; else begin select MIN(N) from remote where center = :my_param and models is null into :min_pos; if (min_pos>0) then update remote set models = :model_id, modelsact="E",modelsstate="M" where N = :min_pos; else insert into remote(center, models, modelsact,modelsstate) values ( :my_param, :model_id, "E","M"); end end else begin /* здесь update вызван самой ЦБ */ for select centerid from centers where working=1 and virtual<>1 and centerid <> :main_cnt into :current_cnt do begin min_pos=0; select MIN(N) from remote where center = :current_cnt and models = :model_id into :min_pos; if (min_pos>0) then update remote set modelsact="M",modelsstate="B" where N = :min_pos; else begin select MIN(N) from remote where center = :current_cnt and models is null into :min_pos; if (min_pos>0) then update remote set models = :model_id, modelsact="M",modelsstate="B" where N = :min_pos; else insert into remote(center, models, modelsact,modelsstate) values ( :current_cnt, :model_id, "M","B"); end end end end else begin /* значит мы являемся УБ, для этого случая другая обработка */ min_pos=0; if ((my_param>0) and (my_param=main_cnt)) then begin /* Этот update пришел от ЦБ то есть со стороны*/ select MIN(N) from remote where center = :main_cnt and models = :model_id into :min_pos; if (min_pos>0) then update remote set modelsact="E",modelsstate="M" where N = :min_pos; else begin select MIN(N) from remote where center = :main_cnt and models is null into :min_pos; if (min_pos>0) then update remote set models = :model_id, modelsact="E",modelsstate="M" where N = :min_pos; else insert into remote(center, models, modelsact, modelsstate) values ( :main_cnt, :model_id, "E","M"); end end else begin /* а этот вызван своей собственной базой */ select MIN(N) from remote where center = :main_cnt and models = :model_id into :min_pos; if (min_pos>0) then update remote set modelsact="M",modelsstate="B" where N = :min_pos; else begin select MIN(N) from remote where center = :main_cnt and models is null into :min_pos; if (min_pos>0) then update remote set models = :model_id, modelsact="M",modelsstate="B" where N = :min_pos; else insert into remote(center, models, modelsact, modelsstate) values ( :main_cnt, :model_id, "M","B"); end end end end end


     Для таблицы CARS триггера должны быть аналогичные.
     Теперь краткое "summary" о целях работы данных триггеров. На первый взгляд они могутпоказаться достаточно громоздкими, однако, на самом деле, довольно посты. Основной задачей для этих триггеров является управление полями с постфиксами ACT и STATE таблицы REMOTE. Поле ACT отображает действие произведенное оператором над записью таблицы, имя которой находится в префиксе этого поля. Например, MODELSACT это действие совершенное пользователем БД в таблице MODELS над записью, ID которой хранится в поле MODEL. Поле с постфиксом STATE является отображением состояния записи таблицы, имя которой является префиксом для STATE, в распределенных операциях БД (проще говоря, состояние процесса репликации). Теперь осмелюсь написать такую фразу: оба этих поля выражают свою информацию для данных своей физической БД по отношению к другой БД, ID центра учета которой, хранится в поле CENTER таблицы REMOTE в этой же строке. Таким образом, используя данные таблицы REMOTE, можно для любой записи либого справочника описать, в каком состоянии находится процесс репликации данных этой записи по отношению к другому центру учета. Поскольку мы договорились, что связей УБД - УБД не существует, то любая УБД знает состояние своих данных через REMOTE только по отношению к одной единственной базе данных - ЦБД. В свою очередь ЦБД знает тоже самое о состоянии синхронизации своих данных, но только не к одной, а ко всем УБД, причем информация об УБД1 и УБД2 и т.д. хранится независимо и может выбираться в зависимости от того какая УБД производит сеанс репликации. Для описания состояния данных в распределенных операциях была принята следующая система кодирования, которую реализуют два вышеприведенных триггера:

1. Система кодирования для ЦБД при добавлении новой записи в справочник, вызванном самой ЦБД
Act State Описание
N (normal) N (normal) Состояние полной 100% синхронизации до того, как новая запись была добавлена
A (added) B (bad) оператор ЦБД добавил новую запись
A (added) S (sent) в процессе репликации информация о добавлении была отослана на УЦУ
N (normal) N (normal) Состояние 100% синхронизации, после успешного цикла репликации


1.1 Система кодирования для УБД при добавлении новой записи, вызванном ЦБД
Act State Описание
- - Записи в справочнике не существует
I (inserted) A (addes) запись добавлена скриптом, который прслала ЦБД
N (normal) N (normal) Состояние 100% синхронизации, после успешного цикла репликации, и отправки подтверждения добавления на ЦБД


2. Система кодирования для ЦБД при модификации записи в справочнике, вызванной ЦБД
Act State Описание
N (normal) N (normal) Состояние полной 100% синхронизации до того, как новая запись была модифицирована
M (modified) B (bad) оператор ЦБД модифицировал запись
M (modified) S (sent) в процессе репликации информация о модификации была отослана на УЦУ
N (normal) N (normal) Состояние 100% синхронизации, после успешного цикла репликации


2.1 Система кодирования для УБД при модификации записи в справочнике, вызванной ЦБД
Act State Описание
N (normal) N (normal) Состояние полной 100% синхронизации до того, как новая запись была модифицирована
E (edited) M (modified) от ЦБД пришел скрипт, который модифицировал запись
N (normal) N (normal) Состояние 100% синхронизации, после успешного цикла репликации, и отсылки подтверждения модификации


     Вышеприведенные таблицы описывают значение полей ACT и STATE таблицы REMOTE, когда воздействие на данные справочников происходит от оператора ЦБД. Соответственно рассмотрим последовательно действия оператора ЦБД, и как это влияет на состояние распределенных операций.
  1. Оператор ЦБД добавляет новую запись в справочник MODELS. Предположим, что ID для этой записи был сгенерирован равным 1. Тогда наши таблицы должны принять следующий вид:

    Таблица MODELS
    MODID NAME DELMARKED PARAM OLDCENTER OLDID
    1 ГАЗ 3110 0 null null null

    Таблица REMOTE
    N CENTER MODELS MODELSACT MODELSSTATE MODELSOLD
    1 2 1 A B null

    Такое значение полей таблицы REMOTE обеспечивается триггером AfterInset.
  2. Во время сеанса репликации ЦБД формирует INI-файл:

    [HEADER]
    FileHeader=Information Exchange INI Protocol v 1.0
    UserName=sysdba
    TimeStamp=01.04.2001 12:00:00
    PackageId=1
    Source=1
    Destination=2
    
    [NEW_RECORDS]
    record0=insert into models(modid,name,delmarked,param,oldcenter,oldid) values(1,"ГАЗ 3110",0,1,1,1)

    Таблица REMOTE после передачи INI-файла при помощи команды
    update remote set modelsstate="S" where modelsact="A" and modelsstate="B" приводится к следующему виду:

    Таблица REMOTE
    N CENTER MODELS MODELSACT MODELSSTATE MODELSOLD
    1 2 1 A S null

  3. УБД принимает этот INI файл, извлекает SQL команду добавления новой записи в справочник MODELS и запускает его в контексте своей базы данных. В результате таблицы УБД принимают следующий вид:

    Таблица MODELS
    MODID NAME DELMARKED PARAM OLDCENTER OLDID
    1 ГАЗ 3110 0 1 1 1


    Таблица REMOTE
    N CENTER MODELS MODELSACT MODELSSTATE MODELSOLD
    1 1 1 I A null

  4. УБД формирует INI файл в сторону ЦБД. Она опрашивает таблицу REMOTE находит запись с признаком добавления ее из ЦБД и отсылает ответ в следующем виде:
    [HEADER]
    FileHeader=Information Exchange INI Protocol v 1.0
    UserName=sysdba
    TimeStamp=01.04.2001 12:01:00
    PackageId=1
    Source=2
    Destination=1
    
    [NEW_RECORDS_CONFIRMED]
    record0=update remote set modelsact="N", modelsstate="N" where center=2 and models=1 and modelsact="A" and modelsstate="S"

    Затем, после завершения отправки INI файла, запускает на своей базе такой SQL:
    update remote set modelsact="N", modelsact="N" where center=1 and models=1 and modelsact="I" and modelsstate="A"
    Давая такую команду, мы как бы заранее предпологаем, что ЦБД приняла и успешно выполнила INI файл, который был ей передан. Почему мы в праве сделать такое предположение, будет понятно далее, когда будет обсуждаться вопрос о разрыве сеанса связи.
  5. ЦБД принимает INI файл с командами от УБД и успешно запускает SQL скрипты из секции [NEW_RECORDS_CONFIRMED] в своем контексте данных. Это приводит ее таблицу REMOTE к следующему виду:

    Таблица REMOTE
    N CENTER MODELS MODELSACT MODELSSTATE MODELSOLD
    1 2 1 N N null

    Таким образом, на этом этапе можно считать, что записи в базах засинхронизированны. Если вы теперь посмотрите на таблицы системы кодирования, которые были приведены мной выше, то увидете полное соответствие переходов полей ACT и STATE таблицы REMOTE тому варианту добавления записи, который я привел в качестве примера.

     Теперь рассмотрим пример синхронизации, вызванный модификацией этой же записи оператором ЦБД.
  1. Оператор ЦБД вносит изменение в название модели автомобиля. Таблицы принимают следующий вид:

    Таблица MODELS
    MODID NAME DELMARKED PARAM OLDCENTER OLDID
    1 ГАЗ 3110 0101 0 null null null

    Таблица REMOTE
    N CENTER MODELS MODELSACT MODELSSTATE MODELSOLD
    1 2 1 M B null

    Такое значение полей таблицы REMOTE обеспечивается триггером AfterUpdate.
  2. Во время сеанса репликации ЦБД формирует INI файл:

    [HEADER]
    FileHeader=Information Exchange INI Protocol v 1.0
    UserName=sysdba
    TimeStamp=01.04.2001 13:00:00
    PackageId=2
    Source=1
    Destination=2
    
    [MODIFIED_RECORDS]
    record0=update models set name="ГАЗ 3110",delmarked=0,param=1 where modid=1

    Таблица REMOTE после передачи INI-файла приводится к следующему виду:

    Таблица REMOTE
    N CENTER MODELS MODELSACT MODELSSTATE MODELSOLD
    1 2 1 M S null

  3. УБД принимает этот INI-файл, извлекает SQL команду модификации записи в справочнике MODELS и запускает ее в контексте своей базы данных. В результате таблицы УБД принимают следующий вид:

    Таблица MODELS
    MODID NAME DELMARKED PARAM OLDCENTER OLDID
    1 ГАЗ 3110 0101 0 1 1 1


    Таблица REMOTE
    N CENTER MODELS MODELSACT MODELSSTATE MODELSOLD
    1 1 1 E M null

  4. УБД формирует INI файл в сторону ЦБД. Она опрашивает таблицу REMOTE находит запись с признаком модификации ее из ЦБД и отсылает ответ в следующем виде:
    [HEADER]
    FileHeader=Information Exchange INI Protocol v 1.0
    UserName=sysdba
    TimeStamp=01.04.2001 13:01:00
    PackageId=2
    Source=2
    Destination=1
    
    [MODIFIED_RECORDS_CONFIRMED]
    record0=update remote set modelsact="N", modelsstate="N" where center=2 and models=1 and modelsact="M" and modelsstate="S"

    Затем, после завершения отправки INI файла, запускает на своей базе такой SQL:
    update remote set modelsact="N", modelsact="N" where center=1 and models=1 and modelsact="E" and modelsstate="M"
    Давая такую команду, мы как бы заранее предпологаем, что ЦБД приняла и успешно выполнила INI файл, который был ей передан. Почему мы в праве сделать такое предположение, будет понятно далее, когда будет обсуждаться вопрос о разрыве сеанса связи.
  5. ЦБД принимает INI файл с командами от УБД и успешно запускает SQL скрипты из секции [NEW_RECORDS_CONFIRMED] в своем контексте данных. Это приводит ее таблицу REMOTE к следующему виду:

    Таблица REMOTE
    N CENTER MODELS MODELSACT MODELSSTATE MODELSOLD
    1 2 1 N N null

    Таким образом, на этом этапе можно считать, что записи в базах засинхронизированны.

     Итак, я подробно описал алгоритм репликации вставки и изменения записей, когда инициатором таких действий выступает ЦБД. Для случая, когда УБД является источником изменений или добавления записей в справочник, система кодирования состояния записей в распределенных операция обсолютно аналогичная, только ЦБД и УБД меняются местами. Реакции на вставку и изменения обслуживают те же самые триггеры. Как вы можете заметить, в них предусмотрено оба варианта работы. Я не буду расписывать подробно происходящие процессы для вставки и изменения записей из УБД, поскольку вы можете это проделать самостоятельно по аналогии с теми примерами, котоые я привел для варианта ЦБД, достаточно только ЦБД и УБД в этих примерах поменять местами с одной существенной оговоркой, что когда ЦБД будет подтверждать вставку записи, еще до SQL подтверждения в INI файле от ЦБД должен быть прописан ключ с номером меньшим чем SQL подтверждения, который поменяет ID записи УБД на глобальный ID, который был присвоен новому элементу генератором ЦБД. То есть секция [NEW_RECORDS_CONFIRMED] должна будет принять следующий вид:
[NEW_RECORDS_CONFIRMED]
record0=update models set modid=1 where modid=3500000000
record1=update remote set modelsact="N", modelsstate="N" where center=2 and models=1 and modelsact="A" and modelsstate="S" 

Здесь мы предпологаем, что УБД присвоила новому элементу ID=3 500 000 000, а ЦБД дала новый глобальный ID=1. Почему это так и зачем это надо, было рассмотрено ранее.

     Теперь, когда мы рассмотрели алгоритм в первом приближении, самое время сопоставить дисциплину связи с нашим алгоритмом репликации.
     Для того, чтобы правильно все встало на свои места, попытайтесь виртуально объединить вариант алгоритма вставки/изменений по инициативе ЦБД и вариант вставки/изменения по инициативе УБД в один сеанс репликации. Если вы правильно это сделали то у вас должно получиться примерно следующее:
Номер этапа Алгоритм репликации Дисциплина связи
Этап первый УБД отправляет свои изменения на ЦБД Пункт № 7
Этап второй ЦБД запускает INI-файл, который она приняла от УБД в контексте своих данных Пункт № 8
Этап третий ЦБД готовит INI-файл о своих произошедших изменениях плис добавляет подтверждения на изменения полученные от УБД Пункт № 9
Этап четвертый ЦБД отправляет свой INI-файл на УБД Пункт № 10,11
Этап пятый УБД принимает INI-файл от ЦБД и запускает внутри себя SQL скрипты вложенные внутрь INI файла от ЦБД Пункт № 12
Этап шестой УБД готовит INI-файл-ответ с подтверждениями на изменения присланные в INI-файе от ЦБД Пункт № 12
Этап седьмой УБД отправляет свой INI-файл-ответ на ЦБД Пункт № 13,14
Этап восьмой ЦБД принимает INI-файл от УБД и запускает его скрипты на выполнение. Затем завершает сеанс репликации. Пункт № 15,16

     Обратите внимание на то обстоятельство, что УБД начинает сеанс репликации первой. Это обусловлено тем, что сервис репликации ЦБД работает в пассивном режиме прослушивая сокет 5000, то есть инициатива исходит от УБД. Кроме того, в силу некоторых исторических традиций и программных решений мне было проще организовать это именно так. Хотя в принципе, все должно зависить от конкретной програмной реализации и принцип работы от этого не изменится.
     Вот, в принципе, мы и замкнули все в одну логическую цепочку. Теперь перейдем к расмотрению некоторых аспектов, связанных с возможностями разрешения потенциальных конфликтов, которые заложены в этом алгоритме.




6. Конфликт вложенности справочников


     Если вы уже забыли про таблицу CARS, то самое время о ней напомнить. Как было заявлено поле MODEL этой таблицы есть ни что иное, как ссылка на таблицу MODELS. В этой связи встает законный вопрос, что будет если новые записи, которые внес оператор одной из баз являются одновременно связанными? Этот конфликт является естественным при репликации связанных справочников. В нашем алгоритме он решается следующим образом.
     Во-первых; все справочники должны быть разделены на иерархию в зависимости от уровней вложенности. На первом месте надо поставить справочники, у которых нет вложенных полей, на второе у которых одно связанное поле, на третье у которых два связанных поля и т.д. В момент формирования SQL скриптов, необходимо так строить ключи record0, record1, record2 .... record*, чтобы обновления от справочников верхнего уровня были запущены на выполнение ранне скриптов относящимся к соответствующим справочникам более низкого уровня. Для нашего случая это означает, что если была добавлена новая модель и, одновременно, эта новая модель была использована при добавления новой записи в CARS, то секция [NEW_RECORDS] должна быть такой:

[NEW_RECORDS]
record0=insert into models(modid,name,delmarked,param,oldcenter,oldid) values(1,"ГАЗ 3110",0,1,1,1)
record1=insert into cars (carid,model,vin,delmarked,param,oldcenter,oldid) values(1,1,"*311000Y001243*",0,1,1,1) 

Поскольку ключ record0 будет выполнен до record1, то все добавленные записи встанут естественным образом.
     Во-торых; есть еще одна тонкость. Все это будет замечательно работать, когда такое изменение происходит из ЦБД, и не сработает, если такая ситуация случится внутри УБД. Обусловлено это необходимостью замены всех ID записей УБД на глобальные ID, которые присваивает ЦБД. Это означает, что УБД, когда формирует скрипты не знает какой ID ЦБД присвоит записи MODELS, чтобы затем подставить ее в CARS. Такая ситуация решается путем предварительного парсинга который выполняется перед запуском скриптов с командой insert и в случае необходимости динамической замены в скрипте ID УБД на только-что сгенерированный глобальный ID предыдущего справочника. Реализуется это в алгоритме ЦБД и только. Для УБД в этом нет необходимости. Конкретная реализация такого парсинга не есть тема данной статьи, поэтому я не привожу текста такой процедуры. Пусть это будет творческим делом каждого. Замечу лишь, что мой вариант работает на основе таблицы RELATIONS, которая была приведена в тексте статьи ранее. Кроме того, принцип наименования полей в REMOTE основан на этом самом динамическом парсинге.




7. Конфликт падения канала связи


     Теперь настало время рассмотреть, как предложенный алгоритм справляется с неожиданными обрывами связи в самый ответственны момент. Не секрет, что наши телефонные линии не страдают особым отсутствием помех. Это означает, что обрыв связи более, чем просто вероятен. Как это ни странно, но данный алгоритм способен практически полностью решать проблемы падения и возобновления репликации без дополнительных надстроек. Происходит это следующим образом.
     Допустим УБД подключилась к ЦБД. Передала добавление новой записи и изменила для этой записи состояние полей ACT и STATE на "A" и "S" соответственно. Далее вошла в режим ожидания ответа от ЦБД. К несчастью в этот момент связь обрывается. Что Произойдет? А собственно ничего страшного, просто в следующий сеанс помимо записей с состояниями ACT="A" и STATE="B" необходимо повторить передачу для записей у которых ACT="A" и STATE="S" и вместо ключей RECORD использовать ключи REPEAT. Это укажет алгоритму ЦБД на то, что это повторная передача. Еще одна деталь: в таблице REMOTE есть поле с постфиксом OLD. Как раз в этой ситуации это поле играет ключевую роль. Если вы внимательно просмотрите триггер AfterInsert, то заметите, что поле OLDID переписывается в поле OLD таблицы REMOTE. Кроме того, в каждом справочнике есть поле OLDCENTER, которое хранит ID ЦУ, который породил эту запись изначально. Все это я веду к тому, что ЦБД, получив скрипт с ключом REPEAT имеет на руках все данные, чтобы решить а надо ли реально повторять этот скрипт, или он был выполнен ранее, достаточно просто соединить таблицы REMOTE с интересующим нас справочником и поискать по OLDCENTER и полю с постфиксом OLD не встречаются ли записи со значениями из ключа REPEAT, который содержит и ID УБД и ID записи, присвоенный УБД. Если окажется, что такая запись уже есть, то нет никакой необходимость добавлять ее вновь. Достаточно просто в таблице REMOTE перевести ее ACT и STATE в состояние только, что добавленной записи ("I" и "A") соответственно, что в процессе формирования ответа, перслать ее глобальный ID и подтверждение об успешном добавлении. Таким образом конфликтная ситуация очень просто и правильно разрешается на стороне как ЦБД так и аналогично на стороне УБД.
     Любая другая ситуация обрыва связи, не представляет собой ничего криминального, и тоже не требует дополнительного принятия мер по обнаружению этапа обрыва репликации. Это следует само собой из алгоритма. Если вы внимательно еще раз проанализируете его работу, то заметите, что каждый из его критических этапов не имеет связи с предыдущим этапом. В этом и состоит главная причина достижения инвариантности процесса репликации к случаям обрыва связи. Таким образом, падение и возобновление связи для этого алгоритма не влечет за собой нарушение целостности данных. А это как раз то, ради чего мы все это городили.




8. Конфликт удаления записей из справочников


      Данный конфликт, вытекает сам собой из самого понятия распределенной БД вообще. Если в некоторый момент времени происходит удаление какого-либо объекта в одной из частей БД, то другая БД об этом удалении ничего не знает. В этом и состоит природа подобного конфликта. Для решения проблем удаления записей, я пошел по принципу трехступенчатого безопасного удаления. Принцип состоит в следующем.
     Любая запись в любом справочнике может иметь три состояния: НОРМА, ПОМЕТКА НА УДАЛЕНИЕ, ЗАЩИТА ПЕРЕД УДАЛЕНИЕМ. НОРМА - это обычное состояние записи. ПОМЕТКА НА УДАЛЕНИЕ - запись, которую пользователь изъявил желание удалить. ЗАЩИТА ПЕРЕД УДАЛЕНИЕМ - запись ожидающая прохождения подтверждения готовности к удалению по всем частям распределенной БД. Когда пользователь хочет удалить какую-либо запись, то он не физически ее удаляет, а помечает на удаление. Для этого служит поле DELMARKED в каждом справочнике. Если там 0, то это НОРМА, если там 1, то эта запись помечена на удаление. В таком состоянии запись может храниться сколько угодно долго. Единственное отличие ее от нормальных записей состоит в том, что она не может участвовать в дальнейших хозяйственных операциях, создаваемых, после того, как запись была помечена на удаление. Если пользователь хочет физически удалить запсь из справочника, то он запускает процедуру удаления помеченных объектов. Эта процедура сканирует местную БД на предмет существования ссылочной зависимости. Если ссылок нет, то запись переходит в режим ЗАЩИТА ПЕРЕД УДАЛЕНИЕМ. Поле DELMARKED=2. В этом режиме запись недоступна для всех интерфейсов БД. Как только происходит первая репликация УБД инфрмирует ЦБД о событии с этой записью. После этого ЦБД сканирует свою базу в поисках ссылок на эту запись. Если ЦБД не находит противоречий, то она в своей базе присваивает этой записи DELMARKED=2. В момент репликации ЦБД и УБД, ЦБД посылает на другие УБД скрипт, который переводит эту запись в такое же состояние. Другие УБД, когда получают сигнал об удалении, так же сканируют свои базы на предмет отсутствия противоречий между своими данными и удалением записи. Если удаление возможно, то УБД отсылает на ЦБД готовность на удаление. Когда ЦБД соберет сигнал готовности на удаление от всех своих УБД, она начинает рассылку скриптов физического удаления этой записи из всех УБД, по мере инициализаций сеансов репликаций от всех своих УБД. Таким образом, присходит нечто вроде транзакции удаления с учетом всех противоречий. Конкретную реализацию такого алгоритма я не привожу, поскольку не ставлю перед собой цель привести готовую программу, а только лишь показать алгоритм, как это можно сделать.
     И еще один намек: те таблицы условных сигналов распределенного состояния записей, которые мной приводились, можно доробатывать и вводить новые состояния, в частности, для состояний распределенного удаления. У меня это так и сделано. Какие сигналы будете использовать вы - это дело вкуса, принцип от этого не меняеется.




9. Проблемы задвоения элементов справочников


     Судя по всему на 100% эта проблема не может быть решена без участия человека. Однако, несколько рекомендаций, тем не менее, можно дать.
     В любом справочнике надо иметь поле НАИМЕНОВАНИЕ, чтобы по нему можно было контролировать уникальность. Но не во всех случаях это приемлемо. Например, фамилии людей повторяются, хотя это люди разные. А для автомобилей поле VIN повторяться не может, поэтому, здесь это может сработать. Сработает это и для справочника моделей автомобилей. Для справочников контрагентов можно дать такую рекомендацию: помино поля НАИМЕНОВАНИЯ (name), заведите поле с КРАТКИМ НАИМЕНОВАНИЕМ (short). Поле SHORT должно быть уникальным и заполняться на основе краткого фнутрифирменного алгоритма преобразования поля NAME. Например, ОАО "Освар" ==> Освар, или Завод резинотехнических изделий ==> заводРТИ. Такое упрощенное наименование используется в отчетах и журналах БД, а НАИМЕНОВАНИЕ используется для печатных форм документов. Иногда оператор создает задвоение объектов, не найдя уже существующего. Поиск по полю SHORT в силу своей простоты, также будет способствовать уменьшению количества задвоенных элементов.
     Если задвоение все же произошло, то необходимо предусмотреть служебную обработку в интерфейсе ответственного оператора, которая сможет объединять элементы справочников в один, и отсылать команду слияния в виде SQL скрипта на удаленные стоянки. Причем, сначала слияние должно произойти на УЦУ, а только потом ЦБД должна выполнить слияние в своих данных. У меня это так и делается. Эта операция так же легко реализуется в рамках рассматриваемого алгоритма. Опять же, оставляю реализацию такой обработки на ваше творчество.




10. Проблемы синхронизации документов


     Документом, я называю некоторый логический объект БД, который связывает объекты справочников в законченную логическую операцию. Обычно документы БД хранятся в журналах операций. По своим принципам синхронизации журналы практически ничем не отличаются от справочников. Однако, в силу своей глубокой индивидуальной реализации и специфики, неотрывно связанной с самой целью существования конкретной БД, совершенно бессмысленно рассматривать здесь реальный пример репликации документов. Поэтому, я опять приведу только рекомендации, а реализацию оставляю на совести разработчиков.
     Прежде всего, в зависимости от уровня вложенности, мы выстроили справочники в иерархию при построении скриптов репликации. В этой схеме журналы документов, должны лежать на самом нижнем уровне и синхронизироваться самыми последними. Это должно быть понятно без дополнительных пояснений. По аналогии со справочниками надо как-то отражать состояние документов в распределенных операциях. Однако незачем создавать еще одну таблицу REMOTE, а уж тем более, пользоваться ей же. Как правило, все документы имеет общие реквизиты (номер документа, дата документа и т.п.). Значит можно создать общий домен для всех документов (обычно я так и делаю). А коль скоро мы имеем общую таблицу, то вполне логично, привязать поля, показывающие распределенное состояние документов, прямо в эту доменную таблицу. Кроме того, проанализируйте свою задачу. Вполне вероятно, что УБД не должны ничего знать ни о данных ЦБД, ни о данных ни одной из УБД. В этом случае проблема упрощается, поскольку один и тот же документ сужествует только на ЦБД и одной и только одной УБД, к которой он относится. Следовательно, не нужно отслеживать состояния этого документа на всех УБД. А это значит, что ID удаленного ЦУ, к которому этот документ относится, можно запихнуть в поле доменной-таблицы. Туда же можно добавить поля для хранения старого ID документа, который ему был назначен соответствующей УБД. Единственное исключение из данного правила, являет собой только документ о перемещении объектов, поскольку перемещение может происходить с УБД1 на УБД2, то этот документ есть одновременно и на УБД1 и на УБД2 и на ЦБД. В этом случае нужен индивидуальный подход. Для меня было проще в доменную таблицу добавить поле для еще одного ID цента учета, а все что с ним связано я разместил в связанной таблице, относящейся только к документам перемещения. Это один из вариантов решения, а на самом деле их может быть много.
     Проблему связанную с удалением документов, можно решать так же как и с удалением справочников. Здесь этот принцип будет нормально работать.
     Подход к решению траблов возникающих из-за исправления документов задним числом, так же должен быть сугубо индивидуален. Для себя я эту проблему решаю путем двухступенчатого прохождения документов по информационной лестнице БД. Первый уровень - это уровень хранения и репликации, второй - уровень проводок. На уровне хранения документ просто существует и никак не влияет на хозяйственные итоги и отчеты. На уровне проводки, документ включается в состав информационных итогов. Это позволяет осуществлять жесткий контроль хозяйственных итогов, независимо от состояния синхронизации. А это как раз одно из необходимых условий устойчивости данных выдаваемых и перерабатываемых БД.