Spec-Zone.ru › CouchDB 3.5

Модель репликации и конфликтов

Рассмотрим следующий пример, иллюстрирующий репликацию и обработку конфликтов.

  • У Алисы есть документ с визитной карточкой Боба;

  • Она синхронизирует его между настольным компьютером и ноутбуком;

  • На настольном компьютере она обновляет адрес электронной почты Боба; не выполняя повторную синхронизацию, она обновляет номер мобильного телефона Боба на ноутбуке;

  • Затем она снова реплицирует их друг с другом.

Таким образом, на настольном компьютере в документе указан новый адрес электронной почты Боба и его старый номер мобильного телефона, а на ноутбуке — старый адрес электронной почты и новый номер мобильного телефона.

Вопрос в том, что произойдёт с этими конфликтующими обновлёнными документами?

Репликация CouchDB

CouchDB работает с документами JSON в базах данных. Репликация баз данных выполняется через HTTP и может быть однонаправленной: «pull» или «push». Поэтому самый простой способ выполнить полную синхронизацию — сначала сделать «push», а затем «pull» (или наоборот).

Итак, Алиса создаёт v1 и синхронизирует её. Она обновляет документ до v2a на одной стороне и до v2b на другой, а затем выполняет репликацию. Что произойдёт?

Ответ прост: обе версии будут существовать с обеих сторон!

DESKTOP                          LAPTOP
+---------+
| /db/bob |                                     INITIAL
|   v1    |                                     CREATION
+---------+

+---------+                      +---------+
| /db/bob |  ----------------->  | /db/bob |     PUSH
|   v1    |                      |   v1    |
+---------+                      +---------+

+---------+                      +---------+  INDEPENDENT
| /db/bob |                      | /db/bob |     LOCAL
|   v2a   |                      |   v2b   |     EDITS
+---------+                      +---------+

+---------+                      +---------+
| /db/bob |  ----------------->  | /db/bob |     PUSH
|   v2a   |                      |   v2a   |
+---------+                      |   v2b   |
                                 +---------+

+---------+                      +---------+
| /db/bob |  <-----------------  | /db/bob |     PULL
|   v2a   |                      |   v2a   |
|   v2b   |                      |   v2b   |
+---------+                      +---------+

В конце концов, это не файловая система, поэтому ничто не запрещает существование нескольких документов с именем /db/bob. Это просто «конфликтующие» редакции с одним и тем же именем.

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

Также стоит отметить, что не нужно настраивать или отслеживать узлы. Можно регулярно выполнять репликацию с узлами или разово отправлять или получать данные по мере необходимости. После завершения репликации нигде не сохраняется информация о том, с какого узла поступил конкретный документ или редакция.

Теперь возникает вопрос: что произойдёт при попытке прочитать /db/bob? По умолчанию CouchDB выбирает одну произвольную редакцию в качестве «победившей», используя детерминированный алгоритм, благодаря которому все узлы сделают одинаковый выбор. То же самое происходит с представлениями: в функцию map передаётся только детерминированно выбранная победившая редакция.

Предположим, что победила v2a. На настольном компьютере, прочитав документ, Алиса увидит v2a — ту версию, которую она там сохранила. Но после репликации на ноутбуке она также увидит только v2a. Может показаться, что сделанные там изменения потерялись, но на самом деле это не так: они просто скрыты в виде конфликтующей редакции. Однако со временем эти изменения нужно будет объединить с визитной карточкой Боба, иначе фактически они будут потеряны.

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

Предотвращение конфликтов

При работе с одним узлом CouchDB предотвращает создание конфликтующих редакций, возвращая ошибку 409 Conflict. Это происходит потому, что при отправке PUT-запроса с новой версией документа необходимо указать _rev предыдущей версии. Если эта _rev уже была заменена, обновление отклоняется с ответом 409 Conflict.

Представим, что два пользователя на одном узле одновременно получают визитную карточку Боба, обновляют её и записывают обратно:

USER1    ----------->  GET /db/bob
         <-----------  {"_rev":"1-aaa", ...}

USER2    ----------->  GET /db/bob
         <-----------  {"_rev":"1-aaa", ...}

USER1    ----------->  PUT /db/bob?rev=1-aaa
         <-----------  {"_rev":"2-bbb", ...}

USER2    ----------->  PUT /db/bob?rev=1-aaa
         <-----------  409 Conflict  (not saved)

Изменения пользователя 2 отклоняются, поэтому приложению нужно снова получить /db/bob и либо:

  1. применить те же изменения, что были внесены в предыдущую редакцию, и отправить новый PUT-запрос;

  2. снова показать документ, чтобы пользователь отредактировал его;

  3. просто перезаписать его ранее сохранённым документом (это не рекомендуется, поскольку изменения пользователя 1 будут незаметно потеряны).

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

Дерево редакций

При обновлении документа в CouchDB сохраняется список предыдущих редакций. Если появляются конфликтующие обновления, история разветвляется в дерево, где текущие конфликтующие редакции документа образуют вершины (листья) этого дерева:

,--> r2a
r1 --> r2b
  `--> r2c

Затем история каждой ветви может продолжаться — например, если прочитать редакцию r2b, а затем выполнить PUT с ?rev=r2b, новая редакция появится именно в этой ветви.

,--> r2a -> r3a -> r4a
r1 --> r2b -> r3b
  `--> r2c -> r3c

В этом примере (r4a, r3b, r3c) — набор конфликтующих редакций. Чтобы разрешить конфликт, нужно удалить конечные узлы остальных ветвей. Поэтому при объединении (r4a+r3b+r3c) в один документ слияния вы замените r4a и удалите r3b и r3c.

,--> r2a -> r3a -> r4a -> r5a
r1 --> r2b -> r3b -> (r4b deleted)
  `--> r2c -> r3c -> (r4c deleted)

Обратите внимание, что r4b и r4c по-прежнему существуют как конечные узлы дерева истории, но в виде удалённых документов. Их можно получить, но они будут помечены как "_deleted":true.

При уплотнении базы данных содержимое всех документов, не являющихся конечными узлами, удаляется. Однако список исторических _revs сохраняется на случай, если в будущем встретятся старые реплики базы данных и потребуется разрешить конфликт. Чтобы этот список не разрастался бесконечно, применяется «очистка истории редакций».

Работа с конфликтующими документами

Базовая операция GET /{db}/{docid} не показывает информацию о конфликтах. Вы видите только детерминированно выбранную победившую редакцию и не получаете сведений о том, существуют ли другие конфликтующие редакции:

{
    "_id":"test",
    "_rev":"2-b91bb807b4685080c6a651115ff558f5",
    "hello":"bar"
}

Если выполнить GET /db/test?conflicts=true, а документ находится в состоянии конфликта, вы получите победившую редакцию и член _conflicts, содержащий массив идентификаторов других конфликтующих редакций. Затем каждую из них можно получить отдельно с помощью последующих операций GET /db/test?rev=xxxx:

{
    "_id":"test",
    "_rev":"2-b91bb807b4685080c6a651115ff558f5",
    "hello":"bar",
    "_conflicts":[
        "2-65db2a11b5172bf928e3bcf59f728970",
        "2-5bc3c6319edf62d4c624277fdd0ae191"
    ]
}

Если выполнить GET /db/test?open_revs=all, вы получите все конечные узлы дерева редакций. Это позволит увидеть все текущие конфликты, а также конечные узлы, которые были удалены (то есть части истории конфликтов, которые уже разрешены). Их можно исключить, отфильтровав документы с "_deleted":true:

[
    {"ok":{"_id":"test","_rev":"2-5bc3c6319edf62d4c624277fdd0ae191","hello":"foo"}},
    {"ok":{"_id":"test","_rev":"2-65db2a11b5172bf928e3bcf59f728970","hello":"baz"}},
    {"ok":{"_id":"test","_rev":"2-b91bb807b4685080c6a651115ff558f5","hello":"bar"}}
]

Тег "ok" — это артефакт open_revs, который также позволяет перечислять конкретные редакции в виде массива JSON, например open_revs=[rev1,rev2,rev3]. В таком виде можно запросить редакцию, которая уже отсутствует, поскольку база данных была уплотнена.

Примечание

Порядок редакций, возвращаемых open_revs=all, НЕ связан с детерминированным алгоритмом выбора «победителя». В приведённом выше примере победившая редакция 2-b91b… возвращается последней, но в других случаях она может находиться на другой позиции.

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

Как описано выше, нужно обновить одну редакцию и явно удалить все конфликтующие редакции. Это можно сделать одним запросом POST к _bulk_docs, задав "_deleted":true для редакций, которые нужно удалить.

API для нескольких документов

Поиск конфликтующих документов с помощью Mango

Добавлено в версии 2.2.0.

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

Вот как найти все конфликты в базе данных:

$ curl -X POST http://adm:pass@127.0.0.1:5984/dbname/_find \
    -d '{"selector": {"_conflicts": { "$exists": true}}, "conflicts": true}' \
    -Hcontent-type:application/json
{"docs": [
{"_id":"doc","_rev":"1-3975759ccff3842adf690a5c10caee42","a":2,"_conflicts":["1-23202479633c2b380f79507a776743d5"]}
],
"bookmark": "g1AAAABheJzLYWBgYMpgSmHgKy5JLCrJTq2MT8lPzkzJBYozA1kgKQ6YVA5QkBFMgKSVDHWNjI0MjEzMLc2MjZONkowtDNLMLU0NzBPNzc3MTYxTTLOysgCY2ReV"}

Значение bookmark можно использовать для перехода к дополнительным страницам результатов, если это необходимо. По умолчанию Mango возвращает только 25 результатов на запрос.

Если вы планируете часто выполнять этот запрос, создайте вторичный индекс Mango, чтобы ускорить поиск:

$ curl -X POST http://adm:pass@127.0.0.1:5984/dbname/_index \
    -d '{"index":{"fields": ["_conflicts"]}}' \
    -Hcontent-type:application/json

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

Поиск конфликтующих документов с помощью индекса _all_docs

Можно получить сразу несколько документов, используя include_docs=true для представления. Однако запрос conflicts=true игнорируется: часть «doc» значения никогда не содержит член _conflicts. Поэтому для определения состояния конфликта каждого документа понадобится выполнить ещё один запрос:

$ curl 'http://adm:pass@127.0.0.1:5984/conflict_test/_all_docs?include_docs=true&conflicts=true'
{
    "total_rows":1,
    "offset":0,
    "rows":[
        {
            "id":"test",
            "key":"test",
            "value":{"rev":"2-b91bb807b4685080c6a651115ff558f5"},
            "doc":{
                "_id":"test",
                "_rev":"2-b91bb807b4685080c6a651115ff558f5",
                "hello":"bar"
            }
        }
    ]
}
$ curl 'http://adm:pass@127.0.0.1:5984/conflict_test/test?conflicts=true'
{
    "_id":"test",
    "_rev":"2-b91bb807b4685080c6a651115ff558f5",
    "hello":"bar",
    "_conflicts":[
        "2-65db2a11b5172bf928e3bcf59f728970",
        "2-5bc3c6319edf62d4c624277fdd0ae191"
    ]
}

Функции map представлений

Представления получают только победившую редакцию документа. Однако если существуют конфликтующие редакции, они также получают член _conflicts. Это позволяет написать представление, задача которого — находить документы с конфликтами. Вот простая функция map, которая это делает:

function(doc) {
    if (doc._conflicts) {
        emit(null, [doc._rev].concat(doc._conflicts));
    }
}

и возвращает следующий результат:

{
    "total_rows":1,
    "offset":0,
    "rows":[
        {
            "id":"test",
            "key":null,
            "value":[
                "2-b91bb807b4685080c6a651115ff558f5",
                "2-65db2a11b5172bf928e3bcf59f728970",
                "2-5bc3c6319edf62d4c624277fdd0ae191"
            ]
        }
    ]
}

В этом случае можно создать отдельный процесс «проверки», который будет периодически сканировать базу данных, искать документы с конфликтами, получать конфликтующие редакции и разрешать конфликты.

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

Кроме того, можно забыть запустить этот процесс или реализовать его неправильно, что приведёт к странному поведению, которое будет сложно отследить.

Алгоритм выбора «победившей» редакции в CouchDB может привести к тому, что информация исчезнет из представления до разрешения конфликта. Снова рассмотрим визитную карточку Боба: предположим, что у Алисы есть представление, которое возвращает номера мобильных телефонов, чтобы её приложение телефонии могло отображать имя звонящего по идентификатору вызывающего абонента. Если существуют конфликтующие документы со старым и новым номерами мобильного телефона Боба, и конфликт разрешается в пользу старого номера, представление не сможет распознать новый номер. В этом случае приложению, возможно, было бы полезнее добавить в представление информацию из обоих конфликтующих документов, но сейчас это невозможно.

Предлагаемый алгоритм получения документа с разрешением конфликтов:

  1. Получить документ с помощью запроса GET docid?conflicts=true.

  2. Для каждого элемента массива _conflicts вызвать GET docid?rev=xxx. Если на этом этапе возникнет ошибка, начать заново с шага 1. (Возможно состояние гонки: кто-то уже разрешил этот конфликт и удалил эту редакцию.)

  3. Выполнить слияние с учётом логики приложения.

  4. Записать _bulk_docs, обновив первую редакцию и удалив остальные.

Это можно делать при каждом чтении (в таком случае все вызовы GET в приложении можно заменить вызовами библиотеки, выполняющей описанные действия) или включить в код процесса проверки.

Вот пример на Ruby с использованием низкоуровневого клиента RestClient:

require "rubygems"
require "rest_client"
require "json"

DB = "http://adm:pass@127.0.0.1:5984/db"

# Write multiple documents
def writem(docs, new_edits)
  JSON.parse(
    RestClient.post(
      "#{DB}/_bulk_docs",
      {:docs => docs, :new_edits => new_edits}.to_json,
      {content_type: :json, accept: :json}
    )
  )
end

# Write one document, return the rev
def write1(doc, id = nil, rev = nil)
  doc["_id"] = id if id
  doc["_rev"] = rev if rev

  if rev
    writem([doc], false)
  else
    writem([doc], true).first["rev"]
  end
end

# Read a document, return *all* revs
def read1(id)
  retries = 0
  loop do
    # FIXME: escape id
    res = [JSON.parse(RestClient.get("#{DB}/#{id}?conflicts=true"))]

    if revs = res.first.delete("_conflicts")
      begin
        revs.each do |rev|
          res << JSON.parse(RestClient.get("#{DB}/#{id}?rev=#{rev}"))
        end

      rescue
        retries += 1
        raise if retries >= 5
        next
      end
    end

    return res
  end
end

# Create DB
RestClient.delete(DB) rescue nil
RestClient.put(DB, {}.to_json)

# Write a document
rev1 = write1({"hello" => "xxx"}, "test")
p(read1("test"))

# Make three conflicting versions
(1..3).each do |num|
  write1({"hello" => "foo"}, "test", rev1 + num.to_s)
  write1({"hello" => "bar"}, "test", rev1 + num.to_s)
  write1({"hello" => "baz"}, "test", rev1 + num.to_s)
end

res = read1("test")
p(res)

# Now let's replace these three with one
res.first["hello"] = "foo+bar+baz"
res.each_with_index do |r, i|
  unless i == 0
    r.replace({"_id" => r["_id"], "_rev" => r["_rev"], "_deleted" => true})
  end
end

writem(res, true)
p(read1("test"))

Приложению, написанному таким образом, никогда не придётся обрабатывать PUT 409, и оно автоматически сможет работать в режиме «мультимастер».

Как видите, если знать, что делать, всё достаточно просто. Просто в CouchDB пока нет удобного HTTP API для «получения всех конфликтующих редакций» или «выполнения PUT-запроса для замены этих N редакций», поэтому такую функциональность нужно реализовать самостоятельно. На момент написания этого текста неизвестно о клиентских библиотеках, поддерживающих эту функциональность.

Слияние и история редакций

Само слияние зависит от конкретного приложения и структуры данных. Иногда это просто: например, если документ содержит список, в который можно только добавлять элементы, то можно объединить две версии списка.

Некоторые стратегии слияния учитывают изменения объекта по сравнению с предыдущей версией. Так работает функция слияния в Git.

Например, чтобы объединить версии визитной карточки Боба v2a и v2b, можно сравнить v1 и v2b, а затем применить эти изменения и к v2a.

В CouchDB иногда можно получить старые редакции документа. Например, если получить /db/bob?rev=v2b&revs_info=true, вы увидите список идентификаторов предыдущих редакций, которые привели к редакции v2b. Сделав то же самое для v2a, можно найти их общего предка. Однако если база данных была уплотнена, содержимое этой редакции документа будет потеряно. revs_info всё ещё покажет, что v1 была предком, но отметит её как «отсутствующую»:

BEFORE COMPACTION           AFTER COMPACTION

     ,-> v2a                     v2a
   v1
     `-> v2b                     v2b

Поэтому для работы с различиями рекомендуется хранить их в самой новой редакции. То есть при замене v1 на v2a добавьте в v2a дополнительное поле или вложение, указывающее, какие поля изменились при переходе от v1 к v2a. К сожалению, это означает, что приложению придётся дополнительно вести учёт изменений.

Сравнение с другими реплицируемыми хранилищами данных

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

Unison

Unison — это инструмент двунаправленной синхронизации файлов. В этом случае визитная карточка была бы файлом, например bob.vcf.

При запуске unison изменения распространяются в обоих направлениях. Если файл изменился на одной стороне, но не на другой, новый файл заменяет старый. Unison хранит локальный файл состояния, чтобы определять, изменился ли файл после последней успешной репликации.

В нашем примере файл изменился с обеих сторон. В файловой системе может существовать только один файл с именем bob.vcf. Unison решает проблему, просто отступая в сторону: пользователь может заменить удалённую версию локальной или наоборот (в обоих случаях данные будут потеряны), но по умолчанию файлы с обеих сторон остаются без изменений.

Для Алисы это, по крайней мере, простое решение. За настольным компьютером она всегда будет видеть версию, которую последней редактировала на настольном компьютере, а за ноутбуком — версию, которую последней редактировала на ноутбуке.

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

Алисе придётся вручную скопировать одну из версий на другую сторону под другим именем файла, объединить их, а затем отправить объединённую версию на другую сторону.

Обратите также внимание, что к этому моменту исходный файл (версия v1) уже потерян. Поэтому, просто осмотрев файлы, нельзя определить, в какой версии — v2a или v2b — указан самый актуальный адрес электронной почты Боба и в какой из них указан самый актуальный номер мобильного телефона. Алисе придётся помнить, какой из них она ввела последним.

Git

Git — известная распределённая система управления версиями. Как и Unison, Git работает с файлами. Однако Git рассматривает состояние целого набора файлов как единый объект — «дерево». При каждом сохранении изменений создаётся «коммит», который ссылается на обновлённое дерево и предыдущие коммиты, а те, в свою очередь, ссылаются на предыдущие деревья. Таким образом сохраняется полная история всех состояний файлов. Эта история образует ветвь, указатель которой хранится на её вершине; по этой истории можно перейти к любому предыдущему состоянию. «Указатель» — это хеш SHA1 коммита на вершине ветви.

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

main               -- my local branch
remotes/foo/main   -- branch on peer 'foo'
remotes/bar/main   -- branch on peer 'bar'

В обычном рабочем процессе репликация выполняется посредством «pull», который импортирует изменения с удалённого узла в локальный репозиторий. Операция «pull» выполняет два действия: сначала «fetch» копирует состояние узла в ветвь отслеживания удалённого узла, а затем пытается «объединить» эти изменения с локальной ветвью.

Теперь рассмотрим визитную карточку. Алиса создала репозиторий Git с bob.vcf и клонировала его на другую машину. Ветви выглядят так, где AAAAAAAA — это SHA1 коммита:

---------- desktop ----------           ---------- laptop ----------
main: AAAAAAAA                        main: AAAAAAAA
remotes/laptop/main: AAAAAAAA         remotes/desktop/main: AAAAAAAA

Затем она вносит изменение на настольном компьютере и фиксирует его в репозитории настольного компьютера; после этого она вносит другое изменение на ноутбуке и фиксирует его в репозитории ноутбука:

---------- desktop ----------           ---------- laptop ----------
main: BBBBBBBB                        main: CCCCCCCC
remotes/laptop/main: AAAAAAAA         remotes/desktop/main: AAAAAAAA

Теперь на настольном компьютере она выполняет git pull laptop. Сначала удалённые объекты копируются в локальный репозиторий и обновляется ветвь отслеживания удалённого узла:

---------- desktop ----------           ---------- laptop ----------
main: BBBBBBBB                        main: CCCCCCCC
remotes/laptop/main: CCCCCCCC         remotes/desktop/main: AAAAAAAA

Примечание

Репозиторий по-прежнему содержит AAAAAAAA, поскольку коммиты BBBBBBBB и CCCCCCCC ссылаются на него.

Затем Git попытается объединить изменения. Зная, что родительский коммит для CCCCCCCC — это AAAAAAAA, он вычисляет различия между AAAAAAAA и CCCCCCCC и пытается применить их к BBBBBBBB.

Если операция завершится успешно, вы получите новую версию с коммитом слияния:

---------- desktop ----------           ---------- laptop ----------
main: DDDDDDDD                        main: CCCCCCCC
remotes/laptop/main: CCCCCCCC         remotes/desktop/main: AAAAAAAA

Затем Алисе нужно войти в систему на ноутбуке и выполнить git pull desktop. Происходит аналогичный процесс. Обновляется ветвь отслеживания удалённого узла:

---------- desktop ----------           ---------- laptop ----------
main: DDDDDDDD                        main: CCCCCCCC
remotes/laptop/main: CCCCCCCC         remotes/desktop/main: DDDDDDDD

Затем выполняется слияние. Это особый случай: CCCCCCCC — один из родительских коммитов DDDDDDDD, поэтому ноутбук может обновиться с помощью fast forward непосредственно от CCCCCCCC до DDDDDDDD, без сложного слияния. В результате получится следующее конечное состояние:

---------- desktop ----------           ---------- laptop ----------
main: DDDDDDDD                        main: DDDDDDDD
remotes/laptop/main: CCCCCCCC         remotes/desktop/main: DDDDDDDD

Всё это хорошо, но может возникнуть вопрос, какое отношение это имеет к CouchDB.

Во-первых, обратите внимание на то, что происходит, если алгоритм слияния завершается неудачно. Изменения всё равно передаются из удалённого репозитория в локальный и становятся доступны в ветви отслеживания удалённого узла. Поэтому, в отличие от Unison, можно быть уверенным, что данные защищены. Просто локальная рабочая копия может не обновиться или отличаться от удалённой версии. Объединённую версию придётся создать и зафиксировать самостоятельно, но при этом гарантированно сохраняется вся история, необходимая для этого.

Обратите внимание, что в Git можно добавлять новые алгоритмы слияния, но стандартные алгоритмы предназначены для построчных изменений исходного кода. Они плохо работают с XML или JSON, если в них нет разрывов строк.

Ещё один интересный момент — наличие нескольких узлов. В этом случае существует несколько ветвей отслеживания удалённых узлов: некоторые могут совпадать с локальной ветвью, некоторые могут отставать от неё, а некоторые — опережать её (то есть содержать изменения, которые ещё не были объединены):

main: AAAAAAAA
remotes/foo/main: BBBBBBBB
remotes/bar/main: CCCCCCCC
remotes/baz/main: AAAAAAAA

Обратите внимание, что каждый узел явно отслеживается, а значит, его нужно явно добавить. Если узел устарел или больше не нужен, его нужно удалить из конфигурации и удалить соответствующую ветвь отслеживания удалённого узла. Это отличается от CouchDB, которая не хранит в базе данных сведения о состоянии узлов.

Ещё одно отличие CouchDB от Git состоит в том, что Git сохраняет всю историю с самого начала: для уменьшения размера Git уплотняет данные, сохраняя различия между всеми версиями, а CouchDB отбрасывает их. Если постоянно обновлять документ, размер репозитория Git будет расти бесконечно. С помощью «переписывания истории» можно заставить Git забыть коммиты, созданные до определённого момента, хотя для этого потребуется приложить некоторые усилия.

Что представляет собой протокол репликации CouchDB? Похож ли он на Git?

Автор:

Jason Smith

Дата:

2011-01-29

Источник:

StackOverflow

Основные положения

Если вы знаете Git, значит, вы знаете, как работает репликация Couch. Репликация очень похожа на отправку или получение данных с помощью распределённых систем управления версиями, таких как Git.

У репликации CouchDB нет собственного протокола. Репликатор просто подключается к двум базам данных как клиент, затем читает данные из одной и записывает в другую. При репликации push считываются локальные данные и обновляется удалённая база данных; при репликации pull происходит обратное.

  • Интересный факт 1: Репликатор — это отдельное приложение Erlang, работающее в собственном процессе. Оно подключается к обеим CouchDB, затем считывает записи из одной и записывает их в другую.

  • Интересный факт 2: CouchDB не может определить, кто является обычным клиентом, а кто — репликатором (и тем более выполняется ли репликация push или pull). Все подключения выглядят как подключения клиентов. Некоторые из них читают записи, другие — записывают.

Всё определяется моделью данных

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

  1. Каждая запись в CouchDB полностью независима от остальных. Это плохо, если вам нужны JOIN или транзакции, но отлично подходит для создания репликатора. Нужно лишь понять, как реплицировать одну запись, и повторить это для каждой записи.

  2. Как и в Git, у записей есть связанная история редакций. Идентификатор редакции записи — это контрольная сумма её данных. Идентификаторы последующих редакций — это контрольные суммы новых данных и идентификатора предыдущей редакции.

  3. Помимо данных приложения ({"name": "Jason", "awesome": true}), каждая запись хранит историю эволюции всех предыдущих идентификаторов редакций, ведущую к ней.

    • Упражнение: ненадолго задумайтесь. Возьмите две разные записи, A и B. Если идентификатор редакции A встречается в истории B, значит, B точно произошла от A. Теперь вспомните быстрое перемещение вперёд при слиянии в Git. Слышите? Это звук взрыва вашего мозга.

  4. На самом деле Git — это не линейный список. В нём есть ответвления, когда у одного родителя несколько дочерних элементов. В CouchDB они тоже есть.

    • Упражнение: сравните две разные записи, A и B. Идентификатор редакции A не встречается в истории B, однако один идентификатор редакции, C, есть в истории и A, и B. Значит, A не произошла от B, и B не произошла от A. Вместо этого у A и B есть общий предок C. В Git это называется «ответвлением». В CouchDB это называется «конфликтом».

    • В Git, если обе дочерние ветви продолжают развиваться независимо, это нормально. Ответвления полностью поддерживаются.

    • В CouchDB, если обе дочерние ветви продолжают развиваться независимо, это тоже нормально. Конфликты полностью поддерживаются.

    • Интересный факт 3: «Конфликты» CouchDB не соответствуют «конфликтам» Git. Конфликт Couch — это расходящаяся история редакций, то, что в Git называется «ответвлением». Поэтому сообщество CouchDB произносит слово «conflict» с немой буквой n: «co-flicked».

  5. В Git также есть слияния, когда у одного дочернего элемента несколько родителей. В CouchDB есть нечто похожее.

    • В модели данных слияния нет. Клиент просто помечает одну историю как удалённую и продолжает работать с единственной оставшейся историей.

    • В приложении это выглядит как слияние. Обычно клиент объединяет данные из каждой истории способом, предусмотренным приложением. Затем он записывает новые данные в историю. В Git это похоже на копирование и вставку изменений из ветви A в ветвь B с последующим созданием коммита в ветви B и удалением ветви A. Данные были объединены, но команда git merge не использовалась.

    • Такое различие обусловлено тем, что в Git важна сама история, тогда как в CouchDB важны данные, а история вторична — она нужна лишь для поддержки репликации. Это одна из причин, по которым встроенная система редакций CouchDB не подходит для хранения данных о версиях, например страниц вики.

Заключительные замечания

По крайней мере одно предложение в этом тексте (возможно, именно это) — полная чушь.

Copyright © 2025 The Apache Software Foundation — Licensed under the Apache License 2.0
https://docs.couchdb.org/en/3.5.1/replication/conflicts.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API