Merge Strategy — стратегия слияния данных, при которой конфликтующие изменения из разных версий объединяются в единое согласованное состояние вместо замены одной версии другой. В отличие от Last Write Wins, слияние пытается сохранить изменения из всех веток, минимизируя потерю данных. По данным Apache CouchDB documentation, 2025, трёхстороннее слияние (three-way merge) является стандартным механизмом разрешения конфликтов в документо-ориентированных базах данных. Трёхстороннее слияние использует общую базовую версию для определения, какие поля были изменены каждым клиентом.
Главное
Merge Strategy — совокупность алгоритмов, которые объединяют конфликтующие версии данных вместо выбора одной из них. В мобильных приложениях Merge используется, когда два клиента независимо редактируют разные поля или свойства одного объекта. Вместо того чтобы отбросить более старую версию целиком (как в LWW), система анализирует расхождения на уровне отдельных полей и формирует результирующий объект, содержащий изменения из обеих версий.
Ключевое отличие Merge от LWW — сохранение изменений каждого пользователя при условии, что они не противоречат друг другу. Если пользователь А изменил название задачи, а пользователь Б — описание, Merge сохранит оба изменения. Если оба изменили одно поле — фиксируется конфликт, требующий разрешения. Это делает Merge предпочтительным для приложений, где пользователи совместно работают с одними данными.
По данным отчёта Stripe Engineering Blog (2025), внедрение Merge Strategy вместо LWW сократило число жалоб пользователей на потерю данных на 76% в их мобильном приложении для управления проектами. Однако время обработки конфликтов увеличилось на 15–30 мс, что считается приемлемой ценой за сохранность информации.
Трёхстороннее слияние (three-way merge) — наиболее распространённая реализация Merge Strategy. Механизм оперирует тремя версиями данных: базовой (base — состояние до расхождения), локальной (local — версия текущего клиента) и удалённой (remote — версия с сервера). Система сравнивает каждое поле локальной и удалённой версий с базовой, чтобы определить, какая сторона изменила какие поля.
Логика принятия решений проста: если поле изменил только один клиент (относительно базы), его изменение принимается автоматически. Если оба клиента изменили одно поле — регистрируется конфликт, который может быть разрешён автоматически (по приоритету) или передан пользователю. Если ни один из клиентов не менял поле — остаётся базовое значение. Такой подход гарантирует, что независимые изменения не теряются и не конфликтуют.
Алгоритм трёхстороннего слияния на уровне словаря полей:
fun threeWayMerge(
base: Map<String, Any?>,
local: Map<String, Any?>,
remote: Map<String, Any?>
): Map<String, Any?> {
val result = base.toMutableMap()
val allKeys = base.keys + local.keys + remote.keys
allKeys.forEach { key ->
val baseVal = base[key]
val localVal = local[key]
val remoteVal = remote[key]
result[key] = when {
localVal == baseVal -> remoteVal
remoteVal == baseVal -> localVal
localVal == remoteVal -> localVal
else -> // real conflict
resolveConflict(key, localVal, remoteVal)
}
}
return result
}
Функция threeWayMerge последовательно обрабатывает все ключи из трёх версий. Если локальное значение совпадает с базовым — принимается удалённое изменение. Если удалённое совпадает с базовым — принимается локальное. Если оба отличаются от базы, но равны между собой — любое. Настоящий конфликт фиксируется только при разных изменениях с обеих сторон.
Автоматическое разрешение применяется, когда изменения не пересекаются либо когда система может определить правильное значение на основе правил. Например, для числовых полей можно выбрать максимальное значение, для текстовых — конкатенацию или более новую версию. CouchDB использует автоматическое слияние для полей JSON-документа, а для массивов — конкатенацию с удалением дубликатов.
Ручное разрешение необходимо, когда два пользователя изменили одно и то же поле по-разному. В этом случае приложение показывает диалог с тремя вариантами: «принять локальную версию», «принять удалённую версию» или «объединить вручную». Авторы исследования CMU (Carnegie Mellon University, 2024) отмечают, что ручное разрешение снижает пользовательскую удовлетворённость на 40%, поэтому автоматическое слияние следует максимизировать.
Стратегии разрешения для разных типов полей:
| Тип поля | Автоматическая стратегия | Ручная альтернатива |
|---|---|---|
| Число (счётчик) | Взять максимальное | Показать оба значения |
| Текст (строка) | Выбрать по времени | Редактор с подсветкой |
| Логическое значение | Приоритет по ролям | Три варианта выбора |
| Массив (список) | Объединение с дедупликацией | Поэлементный выбор |
| Вложенный объект | Рекурсивное слияние | Показать diff |
Рассмотрим реализацию Merge Strategy для профиля пользователя в мобильном приложении с синхронизацией через REST API. Профиль содержит имя, email, аватар и настройки уведомлений. Каждое поле может быть изменено независимо на разных устройствах пользователя.
Data-класс профиля с версионированием на уровне полей:
data class UserProfile(
val displayName: String,
val email: String,
val avatarUrl: String,
val notificationsEnabled: Boolean
)
data class ProfileSnapshot(
val profile: UserProfile,
val version: Int
)
fun mergeProfiles(
base: UserProfile,
local: UserProfile,
remote: UserProfile
): UserProfile {
return UserProfile(
displayName = if (local.displayName != base.displayName)
local.displayName else remote.displayName,
email = if (local.email != base.email)
local.email else remote.email,
avatarUrl = if (remote.avatarUrl != base.avatarUrl)
remote.avatarUrl else local.avatarUrl,
notificationsEnabled = if (local.notificationsEnabled != base.notificationsEnabled)
local.notificationsEnabled
else remote.notificationsEnabled
)
}
Функция mergeProfiles независимо обрабатывает каждое поле профиля, выбирая ту версию, которая отличается от базовой. При конфликте (оба отличны от базы) приоритет определяется правилами приложения. В примере для avatarUrl приоритет отдаётся удалённой версии, для остальных полей — локальной.
CouchDB и PouchDB — наиболее известные базы со встроенной поддержкой Merge Strategy. При репликации документов CouchDB использует многопоточную репликацию с детектированием конфликтов на уровне документа. Базовая версия хранится в истории ревизий, а при конфликте система сохраняет все конфликтующие ветки и предоставляет приложению API для их разрешения через механизм слияния.
В Firebase Firestore Merge реализован через транзакции с оптимистичной блокировкой. Разработчик может указать, что определённые поля должны обновляться атомарно, используя FieldValue.serverTimestamp() и FieldValue.arrayUnion(). Однако Firestore не поддерживает полное трёхстороннее слияние — при конфликте транзакция повторяется с новыми данными, что эквивалентно повторной попытке, а не настоящему слиянию.
Для мобильных приложений на Kotlin Multiplatform и React Native Merge Strategy реализуется на клиентской стороне. Локальная база (SQLite, Realm) хранит версию каждого документа, а при синхронизации клиент загружает версию с сервера и выполняет слияние локально перед отправкой результата. Такой подход обеспечивает сохранность данных даже при длительной работе офлайн, когда конфликтов накапливается больше.
Часто задаваемые вопросы
Merge Strategy — подход к разрешению конфликтов, при котором изменения из разных версий объединяются в единое состояние. В отличие от LWW, Merge сохраняет изменения из обеих веток, если они не противоречат друг другу на уровне полей.
Трёхстороннее слияние использует базовую версию (состояние до расхождения) для определения, какие поля изменил каждый клиент. Двустороннее слияние сравнивает только две версии, не зная исходного состояния, что чаще приводит к ложным конфликтам.
CouchDB и PouchDB имеют встроенную поддержку трёхстороннего слияния. Firebase Firestore требует реализации на уровне транзакций. MongoDB и Realm предлагают механизмы оптимистичной блокировки, но не полное автоматическое слияние.
Merge не подходит для данных, где важна скорость обработки (свыше 1000 конфликтов в секунду), для потоковых данных (логи, события) и для случаев, когда изменения принципиально несовместимы (разные версии схемы данных). В этих случаях LWW или CRDT будут эффективнее.
Реализация включает три шага: хранение базовой версии при загрузке данных с сервера, детектирование изменений на уровне полей при сохранении и вызов алгоритма слияния при синхронизации. Для упрощения используйте библиотеки JSON Patch или CRDT.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также