Merge Strategy — strategia scalania danych, przy której konfliktujące zmiany z różnych wersji są łączone w jeden spójny stan zamiast zastępowania jednej wersji drugą. W przeciwieństwie do Last Write Wins, scalanie stara się zachować zmiany ze wszystkich gałęzi, minimalizując utratę danych. Według Apache CouchDB documentation, 2025, scalanie trójstronne (three-way merge) jest standardowym mechanizmem rozwiązywania konfliktów w dokumentowo-zorientowanych bazach danych. Scalanie trójstronne wykorzystuje wspólną wersję bazową do określenia, które pola zostały zmienione przez każdego klienta.
Najważniejsze
Merge Strategy — zbiór algorytmów, które łączą konfliktujące wersje danych zamiast wybierać jedną z nich. W aplikacjach mobilnych Merge jest używany, gdy dwóch klientów niezależnie edytuje różne pola lub właściwości jednego obiektu. Zamiast odrzucić starszą wersję w całości (jak w LWW), system analizuje rozbieżności na poziomie poszczególnych pól i tworzy wynikowy obiekt zawierający zmiany z obu wersji.
Kluczowa różnica Merge od LWW — zachowanie zmian każdego użytkownika pod warunkiem, że nie są one ze sobą sprzeczne. Jeśli użytkownik A zmienił nazwę zadania, a użytkownik B — opis, Merge zachowa obie zmiany. Jeśli obaj zmienili to samo pole — rejestrowany jest konflikt wymagający rozwiązania. To czyni Merge preferowanym dla aplikacji, w których użytkownicy wspólnie pracują na tych samych danych.
Według raportu Stripe Engineering Blog (2025), wdrożenie Merge Strategy zamiast LWW zmniejszyło liczbę skarg użytkowników na utratę danych o 76% w ich mobilnej aplikacji do zarządzania projektami. Jednak czas przetwarzania konfliktów wzrósł o 15–30 ms, co uważa się za akceptowalną cenę za zachowanie informacji.
Scalanie trójstronne (three-way merge) — najczęściej spotykana implementacja Merge Strategy. Mechanizm operuje na trzech wersjach danych: bazowej (base — stan przed rozbieżnością), lokalnej (local — wersja bieżącego klienta) i zdalnej (remote — wersja z serwera). System porównuje każde pole wersji lokalnej i zdalnej z bazową, aby określić, która strona zmieniła które pola.
Logika podejmowania decyzji jest prosta: jeśli pole zmienił tylko jeden klient (w stosunku do bazy), jego zmiana jest akceptowana automatycznie. Jeśli obaj klienci zmienili to samo pole — rejestrowany jest konflikt, który może być rozwiązany automatycznie (według priorytetu) lub przekazany użytkownikowi. Jeśli żaden z klientów nie zmienił pola — pozostaje wartość bazowa. Takie podejście gwarantuje, że niezależne zmiany nie są tracone i nie powodują konfliktów.
Algorytm scalania trójstronnego na poziomie słownika pól:
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 -> // prawdziwy konflikt
resolveConflict(key, localVal, remoteVal)
}
}
return result
}
Funkcja threeWayMerge sekwencyjnie przetwarza wszystkie klucze z trzech wersji. Jeśli wartość lokalna jest zgodna z bazową — przyjmowana jest zmiana zdalna. Jeśli zdalna jest zgodna z bazową — przyjmowana jest lokalna. Jeśli obie różnią się od bazy, ale są sobie równe — dowolna. Prawdziwy konflikt jest rejestrowany tylko przy różnych zmianach z obu stron.
Automatyczne rozwiązywanie jest stosowane, gdy zmiany nie krzyżują się lub gdy system może określić prawidłową wartość na podstawie reguł. Na przykład dla pól numerycznych można wybrać maksymalną wartość, dla tekstowych — konkatenację lub nowszą wersję. CouchDB używa automatycznego scalania dla pól dokumentów JSON, a dla tablic — konkatenację z usuwaniem duplikatów.
Ręczne rozwiązywanie jest konieczne, gdy dwóch użytkowników zmieniło to samo pole w różny sposób. W tym przypadku aplikacja wyświetla okno dialogowe z trzema opcjami: „przyjmij wersję lokalną”, „przyjmij wersję zdalną” lub „scal ręcznie”. Autorzy badania CMU (Carnegie Mellon University, 2024) zauważają, że ręczne rozwiązywanie obniża satysfakcję użytkownika o 40%, dlatego automatyczne scalanie powinno być maksymalizowane.
Strategie rozwiązywania dla różnych typów pól:
| Typ pola | Strategia automatyczna | Alternatywa ręczna |
|---|---|---|
| Liczba (licznik) | Weź maksymalną | Pokaż obie wartości |
| Tekst (ciąg znaków) | Wybierz według czasu | Edytor z podświetleniem |
| Wartość logiczna | Priorytet według ról | Trzy opcje wyboru |
| Tablica (lista) | Połączenie z deduplikacją | Wybór element po elemencie |
| Obiekt zagnieżdżony | Rekurencyjne scalanie | Pokaż diff |
Rozważmy implementację Merge Strategy dla profilu użytkownika w aplikacji mobilnej z synchronizacją przez REST API. Profil zawiera imię, email, awatar i ustawienia powiadomień. Każde pole może być zmienione niezależnie na różnych urządzeniach użytkownika.
Data-klas profilu z wersjonowaniem na poziomie pól:
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
)
}
Funkcja mergeProfiles niezależnie przetwarza każde pole profilu, wybierając tę wersję, która różni się od bazowej. W przypadku konfliktu (obie różnią się od bazy) priorytet jest określany przez reguły aplikacji. W przykładzie dla avatarUrl priorytet jest przyznawany wersji zdalnej, dla pozostałych pól — lokalnej.
CouchDB i PouchDB — najbardziej znane bazy z wbudowanym wsparciem Merge Strategy. Podczas replikacji dokumentów CouchDB używa wielowątkowej replikacji z wykrywaniem konfliktów na poziomie dokumentu. Wersja bazowa jest przechowywana w historii rewizji, a w przypadku konfliktu system zachowuje wszystkie konfliktujące gałęzie i udostępnia aplikacji API do ich rozwiązywania poprzez mechanizm scalania.
W Firebase Firestore Merge jest zaimplementowany poprzez transakcje z optymistycznym blokowaniem. Deweloper może określić, że określone pola powinny być aktualizowane atomowo, używając FieldValue.serverTimestamp() i FieldValue.arrayUnion(). Jednak Firestore nie obsługuje pełnego scalania trójstronnego — w przypadku konfliktu transakcja jest powtarzana z nowymi danymi, co jest równoważne ponownej próbie, a nie prawdziwemu scalaniu.
Dla aplikacji mobilnych na Kotlin Multiplatform i React Native Merge Strategy jest implementowana po stronie klienta. Lokalna baza (SQLite, Realm) przechowuje wersję każdego dokumentu, a podczas synchronizacji klient pobiera wersję z serwera i wykonuje scalanie lokalnie przed wysłaniem wyniku. Takie podejście zapewnia zachowanie danych nawet podczas długiej pracy offline, gdy konfliktów narasta więcej.
Często zadawane pytania
Merge Strategy — podejście do rozwiązywania konfliktów, w którym zmiany z różnych wersji są łączone w jeden stan. W przeciwieństwie do LWW, Merge zachowuje zmiany z obu gałęzi, jeśli nie są one ze sobą sprzeczne na poziomie pól.
Scalanie trójstronne używa wersji bazowej (stanu przed rozbieżnością) do określenia, które pola zmienił każdy klient. Scalanie dwustronne porównuje tylko dwie wersje, nie znając stanu początkowego, co częściej prowadzi do fałszywych konfliktów.
CouchDB i PouchDB mają wbudowane wsparcie dla scalania trójstronnego. Firebase Firestore wymaga implementacji na poziomie transakcji. MongoDB i Realm oferują mechanizmy optymistycznego blokowania, ale nie pełne automatyczne scalanie.
Merge nie jest odpowiedni dla danych, gdzie ważna jest szybkość przetwarzania (ponad 1000 konfliktów na sekundę), dla danych strumieniowych (logi, zdarzenia) oraz w przypadkach, gdy zmiany są zasadniczo niekompatybilne (różne wersje schematu danych). W takich przypadkach LWW lub CRDT będą skuteczniejsze.
Implementacja obejmuje trzy kroki: przechowywanie wersji bazowej podczas pobierania danych z serwera, wykrywanie zmian na poziomie pól podczas zapisywania i wywołanie algorytmu scalania podczas synchronizacji. Aby uprościć, użyj bibliotek JSON Patch lub CRDT.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również