Реализация конфликт-резолюции при синхронизации данных

TRUETECH занимается разработкой, поддержкой и обслуживанием мобильных приложений iOS, Android, PWA. Имеем большой опыт и экспертизу для публикации мобильных приложений в популярные маркеты Google Play, App Store, Amazon, AppGallery и другие.
Разработка и поддержка любых видов мобильных приложений:
Информационные и развлекательные мобильные приложения
Новостные приложения, игры, справочники, онлайн-каталоги, погодные, фитнес и здоровье, туристические, образовательные, социальные сети и мессенджеры, квиз, блоги и подкасты, форумы, агрегаторы
Мобильные приложения электронной коммерции
Интернет-магазины, B2B-приложения, маркетплейсы, онлайн-обменники, кэшбэк-сервисы, биржи, дропшиппинг-платформы, программы лояльности, доставка еды и товаров, платежные системы
Мобильные приложения для управления бизнес-процессами
CRM-системы, ERP-системы, управление проектами, инструменты для команды продаж, учет финансов, управление производством, логистика и доставка, управление персоналом, системы мониторинга данных
Мобильные приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, платформы предоставления электронных услуг, платформы кешбека, видеохостинги, тематические порталы, платформы онлайн-бронирования и записи, платформы онлайн-торговли

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

Предлагаемые услуги
Показано 1 из 1 услугВсе 1735 услуг
Реализация конфликт-резолюции при синхронизации данных
Сложная
~3-5 рабочих дней
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_mobile-applications_feedme_467_0.webp
    Разработка мобильного приложения для компании FEEDME
    756
  • image_mobile-applications_xoomer_471_0.webp
    Разработка мобильного приложения для компании XOOMER
    624
  • image_mobile-applications_rhl_428_0.webp
    Разработка мобильного приложения для компании RHL
    1052
  • image_mobile-applications_zippy_411_0.webp
    Разработка мобильного приложения для компании ZIPPY
    947
  • image_mobile-applications_affhome_429_0.webp
    Разработка мобильного приложения для компании Affhome
    862
  • image_mobile-applications_flavors_409_0.webp
    Разработка мобильного приложения для компании FLAVORS
    445

Реализация конфликт-резолюции при синхронизации данных

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

Векторные часы и timestamp

Простейшая стратегия — Last Write Wins (LWW): побеждает та запись, у которой timestamp новее. Минус очевиден — при расхождении часов клиентов победит неправильная версия. Клиентские часы ненадёжны: пользователь может перевести время на устройстве.

Надёжный вариант — серверное время. Клиент не доверяет своим часам, а при записи сервер ставит timestamp. Тогда LWW работает корректно.

Более продвинутый подход — векторные часы (Vector Clocks). Каждый клиент имеет идентификатор, и каждое изменение отслеживается вектором версий:

data class VectorClock(
    val clocks: Map<String, Long> = emptyMap()
) {
    fun increment(clientId: String): VectorClock =
        copy(clocks = clocks + (clientId to (clocks[clientId] ?: 0L) + 1))

    fun happensBefore(other: VectorClock): Boolean =
        clocks.all { (k, v) -> v <= (other.clocks[k] ?: 0L) } &&
        clocks != other.clocks

    fun isConcurrentWith(other: VectorClock): Boolean =
        !happensBefore(other) && !other.happensBefore(this)
}

Если clockA.happensBefore(clockB) — версия B позже, берём её. Если isConcurrentWith — конфликт, нужна ручная или автоматическая резолюция.

CRDT для автоматического слияния

CRDT (Conflict-Free Replicated Data Types) — структуры данных, которые можно безопасно объединять без конфликтов математически. Несколько типов:

  • G-Counter — только инкремент. Каждое устройство хранит свой счётчик, итог — сумма всех. Применимо для счётчиков просмотров, лайков.
  • LWW-Register — регистр с Last Write Wins через timestamp. Примитивно, но работает для атомарных значений.
  • OR-Set — набор элементов, где добавление и удаление не конфликтуют.
// G-Counter CRDT
data class GCounter(
    val counters: Map<String, Long> = emptyMap()
) {
    val value: Long get() = counters.values.sum()

    fun increment(nodeId: String, amount: Long = 1): GCounter =
        copy(counters = counters + (nodeId to (counters[nodeId] ?: 0L) + amount))

    fun merge(other: GCounter): GCounter =
        copy(counters = (counters.keys + other.counters.keys).associateWith { key ->
            maxOf(counters[key] ?: 0L, other.counters[key] ?: 0L)
        })
}

Для полноценного использования CRDT в мобильных приложениях есть готовые библиотеки: Automerge (Rust-core, порты для Swift и Kotlin) и Yjs (JavaScript, работает через React Native).

Трёхстороннее слияние (3-way merge)

Лучший подход для текстового контента — как в Git. Нужна общая база (версия до расхождения), изменения клиента A и изменения клиента B.

data class DocumentVersion(
    val id: String,
    val baseVersion: Long,   // версия от которой считаются изменения
    val content: String,
    val patches: List<Patch> // список изменений от base
)

class MergeStrategy {
    fun merge(base: String, clientA: String, clientB: String): MergeResult {
        val patchesA = diff(base, clientA)
        val patchesB = diff(base, clientB)

        val conflicts = findOverlappingPatches(patchesA, patchesB)
        return if (conflicts.isEmpty()) {
            MergeResult.AutoMerged(apply(base, patchesA + patchesB))
        } else {
            MergeResult.Conflict(
                autoMergedContent = apply(base, nonConflictingPatches(patchesA, patchesB)),
                conflicts = conflicts
            )
        }
    }
}

При автоматическом слиянии — применяем обе правки. При пересечении — предлагаем пользователю выбрать или редактировать вручную.

Серверная логика резолюции

Клиент при синхронизации присылает:

{
  "entityId": "note-123",
  "baseVersion": 7,
  "clientVersion": 9,
  "changes": [...],
  "clientId": "device-abc",
  "timestamp": 1712345678000
}

Сервер проверяет текущую версию. Если текущая версия = baseVersion — чистый merge, конфликтов нет, применяем изменения. Если текущая версия > baseVersion — кто-то успел изменить после нашей базы. Сервер возвращает:

{
  "status": "conflict",
  "serverVersion": 10,
  "serverContent": "...",
  "baseContent": "...",
  "clientVersion": 9
}

Клиент получает конфликт и запускает 3-way merge локально.

Стратегии разрешения конфликтов по типам данных

Тип данных Рекомендуемая стратегия
Заметки, документы 3-way merge, ручное разрешение при пересечении
Настройки пользователя LWW с серверным временем
Счётчики (лайки, просмотры) G-Counter CRDT
Корзина покупок OR-Set CRDT (union обеих версий)
Статус заказа Server wins — сервер авторитетен
Позиция на карте LWW

Выбор стратегии не технический, а продуктовый: что важнее — не потерять данные пользователя или не иметь дубликатов?

Хранение истории версий

Для корректной конфликт-резолюции нужна история. Минимум — хранить baseVersion и дельты изменений от неё. При глубоком merge — полная история версий или снепшоты.

@Entity(tableName = "document_versions")
data class DocumentVersionEntity(
    @PrimaryKey val id: String,
    val documentId: String,
    val version: Long,
    val content: String,
    val patch: String,       // JSON-diff от предыдущей версии
    val authorClientId: String,
    val createdAt: Long
)

История версий растёт. Нужна стратегия сжатия: сохранять снепшот каждые N версий, удалять промежуточные после N дней.

Реализация конфликт-резолюции с 3-way merge, CRDT или LWW в зависимости от типов данных: 3–6 недель. Серверная часть — отдельная оценка. Стоимость рассчитывается индивидуально.