Merge Strategy — стратегия за сливане на данни, при която конфликтни промени от различни версии се обединяват в единно съгласувано състояние, вместо да заместят една версия с друга. За разлика от Last Write Wins, сливането се опитва да запази промените от всички клонове, минимизирайки загубата на данни. Според Apache CouchDB documentation, 2025, тристранното сливане (three-way merge) е стандартният механизъм за разрешаване на конфликти в документно-ориентирани бази данни. Тристранното сливане използва обща базова версия за определяне кои полета са променени от всеки клиент.
Основни точки
Merge Strategy — съвкупност от алгоритми, които обединяват конфликтни версии на данни, вместо да изберат една от тях. В мобилните приложения Merge се използва, когато два клиента независимо редактират различни полета или свойства на един обект. Вместо да отхвърли по-старата версия изцяло (както в LWW), системата анализира разликите на ниво отделни полета и формира резултатен обект, съдържащ промени от двете версии.
Ключовата разлика между Merge и LWW — запазване на промените на всеки потребител, при условие че не си противоречат. Ако потребител A е променил името на задачата, а потребител B — описанието, Merge ще запази и двете промени. Ако и двамата са променили едно и също поле — се регистрира конфликт, който изисква разрешаване. Това прави Merge предпочитан за приложения, където потребителите работят заедно с едни и същи данни.
Според доклада на Stripe Engineering Blog (2025), внедряването на Merge Strategy вместо LWW намали броя на жалбите на потребителите за загуба на данни с 76% в тяхното мобилно приложение за управление на проекти. Времето за обработка на конфликти обаче се увеличи с 15–30 ms, което се счита за приемлива цена за запазване на информацията.
Тристранно сливане (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 -> // истински конфликт
resolveConflict(key, localVal, remoteVal)
}
}
return result
}
Функцията threeWayMerge последователно обработва всички ключове от трите версии. Ако локалната стойност съвпада с базовата — се приема отдалечената промяна. Ако отдалечената стойност съвпада с базовата — се приема локалната промяна. Ако и двете се различават от базата, но са равни помежду си — всяка една. Истински конфликт се регистрира само при различни промени от двете страни.
Автоматично разрешаване се прилага, когато промените не се припокриват или когато системата може да определи правилната стойност въз основа на правила. Например за числови полета може да се избере максималната стойност, за текстови полета — конкатенация или по-нова версия. CouchDB използва автоматично сливане за полета на JSON документ, а за масиви — конкатенация с премахване на дубликати.
Ръчно разрешаване е необходимо, когато двама потребители са променили едно и също поле по различен начин. В този случай приложението показва диалог с три опции: „приеми локалната версия“, „приеми отдалечената версия“ или „слей ръчно“. Авторите на изследването на CMU (Carnegie Mellon University, 2024) отбелязват, че ръчното разрешаване намалява удовлетвореността на потребителите с 40%, поради което автоматичното сливане трябва да бъде максимизирано.
Стратегии за разрешаване за различни типове полета:
| Тип поле | Автоматична стратегия | Ръчна алтернатива |
|---|---|---|
| Число (брояч) | Вземи максималното | Покажи и двете стойности |
| Текст (низ) | Избери по време | Редактор с подчертаване |
| Логическа стойност | Приоритет по роли | Три опции за избор |
| Масив (списък) | Обединяване с дедупликация | Поелементен избор |
| Вложен обект | Рекурсивно сливане | Покажи diff |
Нека разгледаме имплементацията на Merge Strategy за потребителски профил в мобилно приложение със синхронизация чрез REST API. Профилът съдържа име, имейл, аватар и настройки за известия. Всяко поле може да бъде променено независимо на различни устройства на потребителя.
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 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също