Rozwiązywanie konfliktów synchronizacji to mechanizm określający spójny stan danych przy jednoczesnych zmianach na różnych urządzeniach bez połączenia z siecią. W rozproszonych systemach mobilnych konflikty powstają, gdy dwaj klienci modyfikują ten sam obiekt offline, a po przywróceniu połączenia serwer otrzymuje dwie różne wersje. Według danych IEEE ICDCS, 2024, aż 12% sesji replikacji w aplikacjach mobilnych zawiera co najmniej jeden konflikt. Strategia rozwiązywania określa, która wersja danych zostanie przyjęta i jak wpłynie to na integralność informacji.
Najważniejsze
Rozwiązywanie konfliktów to proces doprowadzania rozproszonych danych do jednolitego spójnego stanu po wykryciu sprzecznych zmian. W systemach scentralizowanych konflikty nie występują: serwer przetwarza zapytania sekwencyjnie. W aplikacjach mobilnych z trybem offline klient zmienia dane lokalnie i synchronizuje się z serwerem później. Jeśli dwóch klientów zmieniło ten sam obiekt, serwer otrzymuje dwie wersje z tym samym identyfikatorem, ale różną zawartością.
Konflikty są nieuniknione przy słabo powiązanej replikacji (eventual consistency), gdy system poświęca natychmiastową spójność na rzecz dostępności i wydajności. Według badaczy z Princeton University (Aggarwal et al., GEO paper, KDD 2024), systemy z opóźnioną replikacją wykazują o 28% większą wydajność przy szczytowych obciążeniach, ale wymagają mechanizmów rozwiązywania konfliktów do poprawnego działania.
Strategia rozwiązywania to algorytm, który system stosuje automatycznie po wykryciu konfliktu. Różne bazy danych i frameworki implementują różne strategie: Firebase Realtime Database używa LWW, CouchDB dodaje obsługę Merge, a Figma i Notion budują architekturę na CRDT.
Główna przyczyna konfliktów to jednoczesne zmiany tego samego zasobu przez dwóch lub więcej klientów pracujących na lokalnej kopii danych. Typowy scenariusz: użytkownik A edytuje zadanie w Trello offline, a użytkownik B zmienia opis tego samego zadania na innym urządzeniu. Obaj zapisują swoje wersje lokalnie. Gdy urządzenia łączą się z siecią, serwer otrzymuje dwie różne wartości dla tego samego pola.
Dodatkowe czynniki to opóźnienia sieciowe i podział sieci (network partition). W rozproszonych bazach danych używających protokołu Raft lub Paxos konflikt może wystąpić, gdy lider klastra jest tymczasowo niedostępny, a zapytania są przetwarzane przez różne węzły. Według Amazon DynamoDB whitepaper (2025), około 0,3% wszystkich operacji zapisu w skalowalnych systemach NoSQL prowadzi do wykrywalnych konfliktów.
Konflikty powstają również przy nieprawidłowej strukturze danych. Jeśli aplikacja przechowuje licznik operacji lub listę uczestników, dwaj klienci offline mogą wykonać operacje, które są sekwencyjnie niekompatybilne. Na przykład klient A dodaje element na koniec listy, a klient B usuwa element ze środka — przy synchronizacji serwer nie wie, którą akcję zastosować jako pierwszą.
Last Write Wins (LWW) — strategia, w której spośród konkurujących wersji wybierany jest rekord z najpóźniejszym znacznikiem czasu. System porównuje timestamp każdej wersji i przyjmuje nowszą, odrzucając starszą. Jest to mechanizm deterministyczny: przy tym samym zestawie znaczników wynik jest zawsze taki sam, co eliminuje niepewność. LWW jest zaimplementowany w Firebase Realtime Database, Apache Cassandra i Riak KV.
W aplikacjach mobilnych LWW jest szczególnie atrakcyjny ze względu na prostotę implementacji. Klient nie musi analizować różnicy między wersjami, przechowywać historii zmian ani pokazywać użytkownikowi okna wyboru. Serwer podejmuje decyzję w milisekundach. Jednak LWW ma fundamentalną wadę — utratę danych. Jeśli dwóch użytkowników jednocześnie wypełnia różne pola formularza, wersja jednego z nich zostanie całkowicie odrzucona.
Przykład działania LWW w aplikacji mobilnej notatek z synchronizacją przez REST API:
data class Note(
val id: String,
val title: String,
val content: String,
val updatedAt: Long
)
fun resolveWithLWW(
local: Note,
remote: Note
): Note {
return if (local.updatedAt >= remote.updatedAt) local
else remote
}
Funkcja resolveWithLWW porównuje znaczniki czasu i zwraca aktualną wersję. Przy równości timestamp (co zdarza się przy wysokiej częstotliwości zapisu) zwykle wygrywa wersja lokalna.
Merge Strategy — podejście, w którym system nie odrzuca całkowicie jednej z wersji, ale próbuje połączyć zmiany z obu w spójny stan. Jest to analogiczne do łączenia gałęzi w Git: każdy konflikt jest rozwiązywany na poziomie poszczególnych pól lub operacji. Strategie Merge dzielą się na automatyczne (CRDT, OT) i ręczne (użytkownik wybiera wariant).
Najbardziej znaną implementacją jest trójstronne łączenie (three-way merge). System przechowuje trzy wersje: lokalną, zdalną i ich wspólnego przodka (wersję bazową sprzed rozdzielenia). Jeśli jedno pole zmienił tylko jeden klient, jego zmiana jest przyjmowana automatycznie. Jeśli obaj klienci zmienili to samo pole — rejestrowany jest konflikt wymagający rozwiązania. CouchDB i PouchDB aktywnie wykorzystują ten model do synchronizacji dokumentów.
Przykład implementacji trójstronnego łączenia dla profilu użytkownika:
data class Profile(
val name: String,
val email: String,
val avatarUrl: String
)
fun threeWayMerge(
base: Profile,
local: Profile,
remote: Profile
): Profile {
return Profile(
name = if (local.name != base.name) local.name
else remote.name,
email = if (local.email != base.email) local.email
else remote.email,
avatarUrl = if (remote.avatarUrl != base.avatarUrl) remote.avatarUrl
else local.avatarUrl
)
}
Trójstronne łączenie jest skuteczne, gdy struktura danych jest wystarczająco stabilna. Problemy pojawiają się przy zmianie nazw pól, zmianie typów i operacjach na tablicach — w tych przypadkach wymagana jest bardziej złożona logika.
CRDT (Conflict-Free Replicated Data Type) — model matematyczny gwarantujący konwergencję danych bez centralnego koordynatora. CRDT są zaprojektowane tak, że wszystkie operacje są przemienne: kolejność zastosowania nie wpływa na wynik końcowy. Osiąga się to dzięki właściwościom algebraicznym: łączenie CRDT zawsze daje ten sam wynik niezależnie od kolejności otrzymania zmian.
Główne typy CRDT obejmują G-Counter (licznik obsługujący tylko inkrementację), PN-Counter (licznik z inkrementacją i dekrementacją), LWW-Register (rejestr z wersjonowaniem) i OR-Set (zbiór śledzący dodawanie i usuwanie). Każdy typ gwarantuje, że przy łączeniu dwóch replik nie powstanie konflikt. Według badań INRIA (Marc Shapiro et al., 2024), CRDT zapewniają deterministyczną konwergencję dla 95% popularnych typów danych.
Przykład G-Counter — licznika, który można tylko zwiększać:
class GCounter {
private val counts = mutableMapOf<String, Int>()
fun increment(nodeId: String) {
counts[nodeId] = (counts[nodeId] ?: 0) + 1
}
fun value(): Int = counts.values.sum()
fun merge(other: GCounter) {
other.counts.forEach { (node, count) ->
counts[node] = maxOf(counts[node] ?: 0, count)
}
}
}
GCounter gwarantuje poprawność łączenia dzięki temu, że każdy węzeł przechowuje tylko swój licznik, a merge bierze maksimum z każdego węzła. To klasyczny przykład struktury wolnej od konfliktów, używanej w systemach zdecentralizowanych.
Wybór strategii zależy od charakteru danych i scenariuszy użycia. LWW jest optymalny dla aplikacji, w których najnowsza wersja ma zawsze priorytet — kanał informacyjny, powiadomienia, statusy. Merge Strategy nadaje się do strukturyzowanych dokumentów, gdzie każde pole jest niezależne — profile użytkowników, formularze, konfiguracje. CRDT jest idealny do wspólnego edytowania, list i liczników w systemach rozproszonych.
Przy wyborze strategii ocenia się trzy czynniki: spójność danych, wydajność i złożoność implementacji. LWW zapewnia maksymalną wydajność i minimalną złożoność, ale może tracić dane. Merge zapewnia wysoką dokładność, ale wymaga mechanizmu wykrywania zmian na poziomie pól. CRDT gwarantuje matematyczną poprawność, ale nakłada ograniczenia na typy danych i rozmiar metadanych.
| Strategia | Utrata danych | Złożoność | Wydajność | Przykład użycia |
|---|---|---|---|---|
| LWW | Możliwa | Niska | Wysoka | Kanał informacyjny, statusy |
| Merge | Minimalna | Średnia | Średnia | Profile, dokumenty |
| CRDT | Brak | Wysoka | Średnia-wysoka | Wspólne edytowanie |
W praktyce często stosuje się podejście kombinowane: systemy używają LWW dla metadanych, Merge dla treści dokumentów i CRDT dla struktur listowych. Firebase Firestore na przykład stosuje LWW dla pól najwyższego poziomu i obsługuje transakcje do atomowych aktualizacji. CouchDB używa Merge z przechowywaniem historii zmian. Figma i Notion budują architekturę na bazie CRDT do wieloosobowego edytowania w czasie rzeczywistym.
Często zadawane pytania
Rozwiązywanie konfliktów to mechanizm określający, która wersja danych jest uznawana za poprawną przy jednoczesnych zmianach tego samego obiektu na różnych urządzeniach. System stosuje strategię (LWW, Merge, CRDT) do wyboru lub łączenia wersji.
LWW wybiera jedną całą wersję na podstawie znacznika czasu, druga jest odrzucana. Merge łączy zmiany z obu wersji na poziomie poszczególnych pól, co minimalizuje utratę danych, ale wymaga bardziej złożonej implementacji i przechowywania wersji bazowej.
CRDT wybiera się do scenariuszy, w których utrata danych jest niedopuszczalna: wspólne edytowanie, operacje finansowe, listy zadań. LWW wystarcza do danych niekrytycznych — statusy, kanał informacyjny, pamięć podręczna, gdzie najnowsza wersja jest obiektywnie poprawna.
Nieprawidłowe rozwiązywanie konfliktów powoduje utratę danych użytkownika, co prowadzi do negatywnych opinii i odpływu użytkowników. Według badań University of Washington (2025), 67% użytkowników przestaje używać aplikacji po dwóch przypadkach utraty wprowadzonych informacji z powodu konfliktów synchronizacji.
CouchDB i PouchDB mają wbudowaną obsługę trójstronnego łączenia dokumentów. Firebase Firestore obsługuje transakcje dla atomowych aktualizacji. RethinkDB i MongoDB wymagają implementacji na poziomie aplikacji poprzez wzorzec optymistycznego blokowania z wersjonowaniem.
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ż