golang

Не одним CRDT едины или P2P vs Authoritative в local-first приложениях

  • понедельник, 15 сентября 2025 г. в 00:00:08
https://habr.com/ru/articles/946722/

Сегодня поговорим про реализации решения конфликтов в local / offline-first – это когда ваше приложение позволяет пользователям работать полностью или частично оффлайн, а когда они выходят в сеть, синхронизировать все их изменения.

Примеры таких приложений: Notion-like редакторы, Figma-like вайтборды или Linear-like таск менеджеры.

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

Что делать, если 2 человека одновременно поменяли название документа с "Новая папка" на "IT-Качалка Давида Шекунца" и "davids.sh"?

Мы не можем просто смерджить, потому что получится каша в стиле: "IdTav-Кidалка Да.видаshШекунца" – название документа – атомарная сущность.

Может, выбрать вариант, который прислали последним? А если второй потом просто не сможет найти или понять где этот документ? Или вести лог всех изменений, чтобы люди видели что произошло и сами могли поправить? Или сделать поле название как массив строк и дать потом возможность самим выбрать один из вариантов?

А может, мы можем показать человеку это ситуацию и предложить разрешить самому? Или сделать название "(КОНФЛИКТ) IT-Качалка Давида Шекунца || davids.sh" и пусть сами переименуют? А может, назвать одну так и создать вторую папку-ссылку со вторым названием?

Чтобы понять: "как мы будем разруливать конфликты?" – нужно главное понять: "где мы будем это делать?"

P2P vs Authoritative

Есть 2 стула:

  • P2P – то есть каждый клиент способен сам разрулить конфликты

  • Authoritative – кто-то является авторитетом (например, сервер) для разрешения конфликтов

P2P

Чаще всего, реализуется через CRDT – кастомные структуры данных, суть которых в том, что (1) каждая операция над ними описывается как сериализуемый (например, JSON) объект с уникальным идентификатором, (2) неважно в каком порядке и сколько раз набор всех команд будет применен к документу, в итоге, когда у всех будет вся последовательность операций, документ будет выглядеть для всех одинаково.

Конфликты решаются засчет встроенных механизмов работы CRDT структур: если вы выбрали структуру "Последний записавший победитель", то изменение последнего и будут правдой для всех.

Что это дает?

Поскольку все операции в любом порядке и с любым кол-вом дубликатов могут накладываться друг на друга на любом из клиентов:

  • Мы можем построить полностью p2p систему вообще без центрального сервера

  • Конфликтов быть не может, а значит мы не обязаны создавать какие-либо механизмы их разрешения

Из недостатков

  • Поскольку "конфликтов нет", если мы хотим, давать пользователям варианты разрешения спорных ситуаций, мы должны либо создавать специальные структуры (массив названий, вместо одного, где пользователи потом сами выберут корректное) либо лог событий, который мы потом можем анализировать поверх и делать вывод "кажется, здесь конфликт".

  • Либо просто закрывать глаза на такие ситуации дать вероятностям сделать свое дело.

  • Сложность применения правил авторизации (это гигантский отдельный топик)

  • Зависимость от библиотек CRDT (одного стандарта нет, все делают по своему)

  • Проблема часов в распределенных системах

Authoritative

На просторах интернета очень сложно найти альтернативу CRDT-based P2P подходу для реализации local-first collaborative приложений, но на самом деле, такой подход существует:

  • Мы локально работаем с данными и на каждую операцию создаем команду (например, JSON-RPC) или события и если можем, то сразу отправляем, а если нет, то сохраняем в локальный лог команд / событий

  • Когда вышли в сеть отправляем их поочереди на центральный сервер

  • Он берет текущее состояние данных из БД, накладывает присланные команды / события, по заложенной нами логикой пытается разрешить конфликты, но если не может, отправляет обратно "успех" или "неуспех", а результат сохраняет обратно в БД

Точного названия у этого подхода нет, но можно почитать про Replicated State Machine (лучшее объяснение, которое смог найти), Distributed Event Sourcing (опять же, инфы мало, вот интервью с создателем подобной системы) и Centralised server reconciliation (это подход Predict & Reconcile, позаимствованный у игр, вот пример).

Что это дает?

  • Мы также как и с CRDT имеем возможность работать полностью локально

  • Мы можем на каждый конфликт сделать написать любую логику на backend (создать вторую папку с ссылкой на первую, создать нотификацию, переименовать в что-то понятное) или даже вернуть на frontend и разрешить там (на ошибку вывести окно и дать человеку выбрать один из двух вариантов)

  • Хорошо встраивается в стандартный процесс работы frontend-backend: а значит авторизации, мультистэк, транзакции и остальное из коробки

Из недостатков

  • Нужен центральный авторитетный сервер (можно, конечно, построить сложную систему в стиде paxos с передачей лидера между узлами, но это из разряда безумия)

  • Библиотек, реализующих это практически нет, можно выделить только replicache.dev, который прекратил свою поддержку, и livestore.dev, который супер сырой и концепция распределенного event-log пока не кажется жизнеспособной.

  • От парадигмы: "меняем локальные сущности и они автоматически реплицируются и синхронизируются"– мы переходим к: "создаем команды, пытаемся отправить, если не вышло, ручками меняем что-то локально (predict / optimistic) и ждем синхронизации, чтобы выкинуть команду из лога или откатиться" – и в первом случае реально было бы достаточно `doc.update({ title: "davids.sh"})`, то во втором придется написать очень много оберточного кода

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

В заключении

Делать подобного рода системы – сложно, сложнее, чем стандартный online или online-first (online + optimistic state), поэтому без надобности в такое точно не стоит лезть.

А если и полезли, то помните, что вы не сможете просто перенести стандартные модели online приложений, вам придется учиться подстраивать архитектуру и бизнес-логику под такой формат.

Еще вот вам 2 очень крутые ссылочки на разные технологии для реализации local / offline-first:

  1. https://electric-sql.com/docs/reference/alternatives

  2. https://www.localfirst.fm/landscape

P.S.

Если вам интересна тема local-first приложений, приглашаю в свой паблик в телеграмме 🦾 IT-Качалка Давида Шекунца 💪, там скоро я начну выпускать много подобного контента по новому крутому проекту над которым мы работаем.