CRDT - dlaczego świat migruje w stronę danych, które same rozwiązują konflikty
Poniedziałek, 9:00. Dwóch handlowców edytuje ten sam cennik w firmowym systemie. Adam zmienia cenę produktu A w Warszawie. Beata zmienia cenę produktu B w Krakowie. Obaj klikają "Zapisz" w tym samym momencie. Jeden z nich dostaje komunikat: "Błąd: rekord został zmieniony przez innego użytkownika. Odśwież stronę i wprowadź zmiany ponownie."
Beata odświeża. Jej 15 minut pracy zniknęło. Wpisuje zmiany od nowa. W międzyczasie Adam zmienił kolejną cenę. Beata znowu dostaje błąd. Dzwoni do IT. IT mówi: "Nie edytujcie tego samego cennika jednocześnie."
W 2026 roku "nie edytujcie jednocześnie" to nie rozwiązanie. To kapitulacja.
Istnieje matematycznie udowodniony sposób, żeby Adam i Beata mogli edytować ten sam cennik jednocześnie, offline, z różnych kontynentów - i żeby system sam połączył ich zmiany bez żadnego konfliktu. Nazywa się CRDT.
Czym jest CRDT
CRDT - Conflict-free Replicated Data Type - to struktura danych zaprojektowana tak, żeby mogła istnieć w wielu kopiach jednocześnie, być modyfikowana niezależnie na każdej kopii, bez koordynacji między nimi, i zawsze zbiegać do tego samego stanu.
Nie "zwykle". Nie "w większości przypadków". Zawsze. To gwarancja matematyczna, nie obietnica marketingowa.
Jak to działa - intuicja
Wyobraź sobie licznik odwiedzin strony. Masz 3 serwery. Każdy liczy odwiedziny lokalnie:
- Serwer A: 150 odwiedzin
- Serwer B: 230 odwiedzin
- Serwer C: 90 odwiedzin
Gdy serwery się zsynchronizują, wynik to 150 + 230 + 90 = 470. Nie ma konfliktu. Nie ma "który serwer ma rację". Każdy dodał swoje, suma jest jednoznaczna. Kolejność synchronizacji nie ma znaczenia - A zsynchronizuje się z B, potem z C, albo C z A, potem z B. Wynik jest ten sam: 470.
To jest G-Counter - najprostszy CRDT. Grow-only counter. Ale ta sama zasada skaluje się do map, zbiorów, rejestrów, a nawet dokumentów tekstowych.
Dwie rodziny CRDT
| Rodzina | Jak działa | Kiedy stosować |
|---|---|---|
| State-based (CvRDT) | Repliki wymieniają pełny stan (lub deltę). Merge jest komutacyjny, asocjacyjny i idempotentny - kolejność i liczba merge'ów nie ma znaczenia | Proste sieci, tolerancja na większe payloady |
| Operation-based (CmRDT) | Repliki wymieniają operacje (np. "dodaj element X"). Operacje muszą być komutatywne | Gdy payloady muszą być małe, sieć gwarantuje dostarczenie |
W praktyce większość implementacji w Elixirze używa delta-state CRDT - hybrydy, która wysyła tylko zmiany (delty) zamiast pełnego stanu, łącząc zalety obu podejść.
Najważniejsze typy CRDT
| Typ | Opis | Zastosowanie |
|---|---|---|
| G-Counter | Licznik rosnący (tylko inkrementacja) | Zliczanie odwiedzin, eventów, kliknięć |
| PN-Counter | Dwa G-Countery (wzrost + spadek) | Stany magazynowe, pule zasobów, salda |
| LWW-Register | Rejestr "wygrywa ostatni zapis" | Pola profilu użytkownika, ustawienia konfiguracji |
| OR-Set | Zbiór z operacjami dodaj/usuń i śledzeniem przyczynowości | Tagi, listy członków, koszyk zakupowy |
| AWLWWMap | Mapa klucz-wartość z semantyką add-wins | Rozproszone słowniki, rejestry konfiguracji |
Dlaczego świat migruje w stronę CRDT
Cztery siły napędzają tę zmianę jednocześnie.
1. Ruch local-first
Ink & Switch, laboratorium badawcze stojące za jednym z najważniejszych esejów ostatniej dekady ("Local-first software"), sformułowało prostą tezę: użytkownik powinien być właścicielem swoich danych, aplikacja powinna działać offline, a współpraca powinna odbywać się peer-to-peer.
CRDT to fundament technologiczny tego podejścia. Bez CRDT local-first jest życzeniem. Z CRDT - jest architekturą.
Społeczność local-first w 2026 roku jest porównywana do Reacta w 2013. Wcześnie, ale kierunek jest oczywisty.
2. Oczekiwanie współpracy w czasie rzeczywistym
Google Docs przyzwyczaiło ludzi do jednoczesnej edycji. Teraz każda aplikacja, która blokuje jednoczesny dostęp, wygląda jak relikt. Problem: Google Docs używa Operational Transform (OT), która wymaga centralnego serwera. CRDT osiąga ten sam efekt bez centralnego koordynatora - co oznacza, że działa także offline i peer-to-peer.
3. Edge computing i offline-first
Połowa firm wdrażających local-first potrzebuje offline'u dla pracowników w terenie - handlowców, serwisantów, kontrolerów jakości. Druga połowa chce szybszego UI - operacje lokalne z natychmiastową odpowiedzią, synchronizacja w tle.
CRDT obsługuje oba scenariusze, bo stan można modyfikować lokalnie i mergować później - bez ryzyka utraty danych, bez ręcznego rozwiązywania konfliktów.
4. Systemy rozproszone jako domyślna architektura
Aplikacje działają w wielu data center, regionach, na wielu urządzeniach. Konsensus rozproszony (Paxos, Raft) na każdym zapisie to latencja i złożoność. CRDT eliminuje potrzebę konsensusu - każda replika zapisuje lokalnie, synchronizacja jest asynchroniczna, spójność jest gwarantowana matematycznie.
CRDT vs tradycyjne podejścia
CRDT vs Operational Transform (OT)
| Wymiar | CRDT | Operational Transform |
|---|---|---|
| Architektura | Zdecentralizowana, peer-to-peer | Wymaga centralnego serwera |
| Offline | Natywny - edytuj lokalnie, merguj później | Słaby - serwer potrzebny do transformacji |
| Gwarancja spójności | Matematyczna (strong eventual consistency) | Algorytmiczna (funkcje transformacji) |
| Złożoność | W projekcie typu danych | W algorytmie transformacji |
| Skalowalność | Liniowa - każdy nowy węzeł to niezależna replika | Ograniczona przez centralny serwer |
| Dojrzałość | Nowsza, rosnący ekosystem | Dekady produkcyjnego użycia (Google Docs, Etherpad) |
Kluczowy kompromis: OT lepiej zachowuje intencję użytkownika (wynik wygląda tak, jak użytkownicy oczekują), ale wymaga centralnego serwera. CRDT gwarantuje zbieżność bez koordynacji, ale wynik nie zawsze idealnie odpowiada intencji. W praktyce, dla większości zastosowań biznesowych, CRDT jest wystarczające i znacznie prostsze operacyjnie.
CRDT vs Last-Write-Wins
Last-Write-Wins (LWW) to najprostszy CRDT - rejestr, w którym wygrywa zapis z najnowszym timestampem. Problem: współbieżne zapisy są tracone. Adam zmienił cenę, Beata zmieniła opis - wygrywa jedno, drugie znika.
Bogatsze typy CRDT (OR-Set, PN-Counter, AWLWWMap) zachowują wszystkie współbieżne operacje i mergują je semantycznie. Adam zmienił cenę, Beata zmieniła opis - obie zmiany są zachowane, bo dotyczą różnych pól.
CRDT vs konsensus rozproszony (Paxos/Raft)
| Wymiar | CRDT | Konsensus (Paxos/Raft) |
|---|---|---|
| Dostępność | Zawsze (AP w CAP) | Wymaga kworum (CP w CAP) |
| Latencja zapisu | Lokalna, asynchroniczna synchronizacja | Synchroniczna zgoda przed zapisem |
| Partycja sieciowa | Obie strony piszą dalej | Mniejszość nie może pisać |
| Typy operacji | Ograniczone do mergowalnych typów | Dowolne |
| Zastosowanie | Liczniki, zbiory, presence, konfiguracja | Elekcja lidera, replikacja logów, transakcje |
CRDT i konsensus to nie konkurencja - to narzędzia do różnych problemów. CRDT tam, gdzie dostępność jest ważniejsza niż natychmiastowa spójność. Konsensus tam, gdzie potrzebujesz jednego źródła prawdy w danym momencie.
CRDT w Elixirze i Phoenixie
Elixir nie "wspiera" CRDT jako dodatkową bibliotekę. CRDT jest wbudowane w fundamenty ekosystemu. Phoenix.Presence - mechanizm śledzenia obecności użytkowników - to CRDT. Horde - rozproszony supervisor - to CRDT. Nawet konfiguracja klastra w wielu aplikacjach opiera się na CRDT.
Phoenix.Presence - CRDT, którego używasz nie wiedząc o tym
Phoenix.Presence to prawdopodobnie najszerzej używana implementacja CRDT w ekosystemie Elixira. Każda aplikacja Phoenix z funkcjonalnością "kto jest online" używa CRDT pod spodem.
Architektura:
- Zero single point of failure
- Zero centralnego źródła prawdy
- Zero zewnętrznych zależności (bez Redisa, bez bazy danych)
- Samonaprawianie przez zbieżność CRDT
Pod spodem Phoenix.Presence używa Phoenix.Tracker, który łączy protokół heartbeat/gossip z delta-state CRDT. Każdy węzeł klastra prowadzi własną replikę stanu obecności i rozgłasza delty w regularnych interwałach.
defmodule MyApp.Presence do
use Phoenix.Presence,
otp_app: :my_app,
pubsub_server: MyApp.PubSub
endTo jest cała konfiguracja. Od tego momentu masz rozproszony, samonaprawiający się system śledzenia obecności bez żadnej zewnętrznej infrastruktury.
Śledzenie obecności w LiveView:
def mount(%{"doc_id" => doc_id}, _session, socket) do
topic = "doc:#{doc_id}"
if connected?(socket) do
MyApp.Presence.track(self(), topic, socket.assigns.current_user.id, %{
name: socket.assigns.current_user.name,
cursor: %{x: 0, y: 0},
joined_at: DateTime.utc_now()
})
Phoenix.PubSub.subscribe(MyApp.PubSub, topic)
end
presences = MyApp.Presence.list(topic)
{:ok, assign(socket, presences: presences)}
end
def handle_info(%Phoenix.Socket.Broadcast{event: "presence_diff", payload: diff}, socket) do
presences =
socket.assigns.presences
|> MyApp.Presence.sync_diff(diff)
{:noreply, assign(socket, presences: presences)}
endTrzy węzły Phoenix, 500 użytkowników, zero Redisa. Użytkownik dołącza na węźle A - węzły B i C wiedzą o tym w ciągu milisekund. Węzeł A pada - informacja o rozłączonych użytkownikach propaguje się przez CRDT. Zero koordynacji, zero lidera, zero single point of failure.
Parametry konfiguracyjne Trackera:
| Parametr | Domyślna wartość | Co robi |
|---|---|---|
:broadcast_period | 1500ms | Interwał rozgłaszania delt |
:max_silent_periods | 10 | Periody ciszy przed wykryciem awarii (~15s) |
:down_period | 30s | Tymczasowe oznaczenie węzła jako offline |
:permdown_period | 20 min | Permanentne usunięcie z repliki |
:pool_size | 1 | Liczba shardów trackera |
DeltaCrdt - CRDT ogólnego przeznaczenia
delta_crdt to główna biblioteka CRDT w Elixirze, stworzona przez Dereka Kraana. Implementuje delta-state CRDT z drzewem Merkle'a do efektywnej synchronizacji - zamiast wysyłać pełny stan, wysyła tylko zmiany od ostatniej synchronizacji.
# mix.exs
def deps do
[{:delta_crdt, "~> 0.6"}]
endPodstawowe użycie - rozproszona mapa klucz-wartość:
# Uruchom dwa węzły CRDT (AWLWWMap = Add-Wins Last-Write-Wins Map)
{:ok, crdt1} = DeltaCrdt.start_link(DeltaCrdt.AWLWWMap, sync_interval: 50)
{:ok, crdt2} = DeltaCrdt.start_link(DeltaCrdt.AWLWWMap, sync_interval: 50)
# Ustaw dwukierunkową synchronizację
DeltaCrdt.set_neighbours(crdt1, [crdt2])
DeltaCrdt.set_neighbours(crdt2, [crdt1])
# Zapis na węźle 1
DeltaCrdt.put(crdt1, "cena_produkt_A", 199_90)
# Po sync_interval (~50ms) węzeł 2 ma dane
DeltaCrdt.get(crdt2, "cena_produkt_A")
# => 199_90
# Jednoczesny zapis na obu węzłach - różne klucze
DeltaCrdt.put(crdt1, "cena_produkt_A", 179_90)
DeltaCrdt.put(crdt2, "cena_produkt_B", 299_90)
# Po synchronizacji oba węzły mają obie zmiany
DeltaCrdt.to_map(crdt1)
# => %{"cena_produkt_A" => 179_90, "cena_produkt_B" => 299_90}
DeltaCrdt.to_map(crdt2)
# => %{"cena_produkt_A" => 179_90, "cena_produkt_B" => 299_90}W drzewie supervisorów:
children = [
{DeltaCrdt, [crdt: DeltaCrdt.AWLWWMap, name: MyApp.ConfigStore]}
]
Supervisor.start_link(children, strategy: :one_for_one)
# Teraz cała aplikacja ma dostęp do rozproszonego store'a
DeltaCrdt.put(MyApp.ConfigStore, :feature_flags, %{new_ui: true, beta_api: false})Praktyczny wzorzec - rozproszona konfiguracja runtime:
defmodule MyApp.RuntimeConfig do
@moduledoc """
Konfiguracja runtime replikowana automatycznie na wszystkie węzły klastra.
Zmiana na jednym węźle propaguje się do wszystkich w ciągu milisekund.
"""
def child_spec(_opts) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, []},
type: :worker
}
end
def start_link do
{:ok, pid} = DeltaCrdt.start_link(DeltaCrdt.AWLWWMap,
name: __MODULE__,
sync_interval: 100,
on_diffs: fn diffs ->
# Reakcja na zmiany z innych węzłów
for {key, _old, new} <- diffs do
Phoenix.PubSub.broadcast(MyApp.PubSub, "config:#{key}", {:config_changed, key, new})
end
end
)
# Połącz z innymi węzłami w klastrze
neighbours = Node.list() |> Enum.map(&{__MODULE__, &1})
DeltaCrdt.set_neighbours(pid, neighbours)
{:ok, pid}
end
def get(key), do: DeltaCrdt.get(__MODULE__, key)
def put(key, value), do: DeltaCrdt.put(__MODULE__, key, value)
def delete(key), do: DeltaCrdt.delete(__MODULE__, key)
def all, do: DeltaCrdt.to_map(__MODULE__)
endZmiana feature flaga na jednym węźle - propagacja na cały klaster bez restartu, bez deploymentu, bez zewnętrznej bazy konfiguracji. CRDT gwarantuje, że wszystkie węzły będą miały identyczny stan.
Horde - rozproszony supervisor na CRDT
Horde to rozproszone wersje DynamicSupervisor i Registry z biblioteki standardowej Elixira, zbudowane na DeltaCrdt. Drop-in replacement - ten sam interfejs, rozproszone działanie.
defmodule MyApp.Application do
use Application
def start(_type, _args) do
children = [
{Horde.DynamicSupervisor,
[name: MyApp.DistributedSupervisor, strategy: :one_for_one]},
{Horde.Registry,
[name: MyApp.DistributedRegistry, keys: :unique]}
]
Supervisor.start_link(children, strategy: :one_for_one)
end
endJeden proces na klucz w całym klastrze:
# Uruchom proces obsługujący klienta - gdziekolwiek w klastrze
Horde.DynamicSupervisor.start_child(
MyApp.DistributedSupervisor,
{MyApp.CustomerSession, customer_id: "cust_123"}
)
# Znajdź ten proces z dowolnego węzła
[{pid, _}] = Horde.Registry.lookup(MyApp.DistributedRegistry, "cust_123")
GenServer.call(pid, :get_state)Co się dzieje, gdy węzeł pada:
- CRDT wykrywa brak heartbeatu z węzła
- Informacja propaguje się do wszystkich replik
- Horde automatycznie restartuje procesy z upadłego węzła na żywych węzłach
- Registry aktualizuje się - PID-y wskazują na nowe lokalizacje
- Aplikacja działa dalej bez interwencji operatora
To jest kluczowe: CRDT nie jest tu ciekawostką akademicką. Jest fundamentem, na którym działa automatyczna redystrybucja procesów w klastrze. Bez CRDT Horde potrzebowałby centralnego koordynatora (single point of failure) albo protokołu konsensusu (wolniejszy, bardziej złożony).
Rozwiązywanie konfliktów nazw w Registry:
Gdy partycja sieciowa powoduje, że dwa węzły rejestrują ten sam klucz, CRDT po reunifikacji wykrywa duplikat. Horde wybiera zwycięzcę i wysyła przegranej stronie sygnał wyjścia:
# W module procesu - obsługa przegranej w konflikcie nazwy
def handle_info({:name_conflict, {name, _value}, _registry, winning_pid}, state) do
# Opcjonalnie: przenieś stan do zwycięskiego procesu
GenServer.cast(winning_pid, {:merge_state, state})
{:stop, :name_conflict, state}
endPhoenix.Sync i ElectricSQL - CRDT między serwerem a klientem
Najnowszy gracz w ekosystemie. ElectricSQL to warstwa synchronizacji, która łączy PostgreSQL z przeglądarką lub aplikacją mobilną. Silnik synchronizacji napisany jest w Elixirze. Phoenix.Sync to oficjalna integracja z Phoenixem.
# mix.exs
def deps do
[{:phoenix_sync, "~> 0.6"}]
endDwa tryby pracy:
| Tryb | Jak działa | Kiedy używać |
|---|---|---|
| Embedded | Electric działa jako zależność aplikacji, streamuje dane przez wewnętrzne API Elixira | Prosta architektura, jeden deployment |
| HTTP | Łączy się z zewnętrznym serwisem Electric przez HTTP | Osobne skalowanie sync engine'u |
ElectricSQL czyta WAL (Write-Ahead Log) PostgreSQL i streamuje zmiany do klientów. Klienci pracują na lokalnej bazie (PGlite, SQLite). Zmiany lokalne propagują się z powrotem do PostgreSQL. Konflikty rozwiązywane automatycznie.
To domyka pętlę: CRDT nie tylko między węzłami BEAM w klastrze, ale między serwerem a urządzeniami końcowymi - przeglądarkami, telefonami, tabletami. Handlowiec w terenie bez zasięgu edytuje dane lokalnie. Dane synchronizują się automatycznie po odzyskaniu połączenia. Zero konfliktów, zero utraconej pracy.
Dlaczego BEAM jest naturalnym domem dla CRDT
CRDT w Javie, Go czy Node.js to biblioteka, którą dodajesz do runtime'u, który nie był projektowany z myślą o dystrybucji. CRDT na BEAM to naturalne rozszerzenie maszyny wirtualnej, która od 1986 roku jest budowana do systemów rozproszonych.
Procesy BEAM to naturalne repliki CRDT
Procesy BEAM nie dzielą pamięci. Komunikują się przez wiadomości. Każdy proces może trzymać własną replikę CRDT, a mergowanie odbywa się przez passing wiadomości - bez locków, bez współdzielonego mutable state.
W Javie potrzebujesz ConcurrentHashMap, synchronized bloków, volatile pól. W Elixirze - wysyłasz wiadomość.
Transparentna dystrybucja
BEAM jest jedyną szeroko używaną maszyną wirtualną z wbudowanym modelem dystrybucji. Wysłanie wiadomości do procesu na innym serwerze wygląda identycznie jak wysłanie do procesu lokalnego:
# Lokalna wiadomość
send(local_pid, {:merge, delta})
# Zdalna wiadomość - ten sam kod
send(remote_pid, {:merge, delta})DeltaCrdt na węźle A synchronizuje się z DeltaCrdt na węźle B dokładnie tym samym mechanizmem co z procesem lokalnym. Zero konfiguracji sieciowej, zero serializacji, zero dodatkowych warstw transportowych.
Garbage collector per-proces
GC na BEAM działa per-proces - sub-milisekundowe pauzy, izolowane do jednego procesu. To krytyczne dla CRDT, gdzie operacje merge'owania są częste i nieprzewidywalne. W Javie stop-the-world GC może wstrzymać całą synchronizację CRDT w klastrze. Na BEAM - merge w procesie A nie wpływa na merge w procesie B.
Preemptywny scheduler
Scheduler BEAM wymusza sprawiedliwy podział CPU (~4000 redukcji na timeslice). Duży merge CRDT nie zagłodzi innych procesów. W Node.js (single-threaded event loop) ciężki merge blokuje cały serwer. W BEAM - jest jednym z milionów procesów, każdy z gwarantowanym czasem CPU.
Let it crash + CRDT = self-healing
Proces CRDT pada? Supervisor restartuje go. Zrestartowany proces synchronizuje się z sąsiadami i odzyskuje pełny stan. To jest beauty CRDT na BEAM - odporność na awarie jest wbudowana w oba systemy jednocześnie:
- BEAM: supervisor restartuje proces
- CRDT: re-synchronizacja odbudowuje stan
Żaden inny runtime nie daje obu tych gwarancji out of the box.
Zastosowania w praktyce
| Zastosowanie | Implementacja | Szczegóły |
|---|---|---|
| Śledzenie obecności | Phoenix.Presence | Kto jest online, pozycje kursorów, wskaźniki "pisze..." - bez Redisa, bez bazy |
| Rozproszone procesy | Horde (DeltaCrdt) | Jeden proces na klucz w całym klastrze - sesje użytkowników, urządzenia IoT, pokoje czatu |
| Konfiguracja runtime | DeltaCrdt | Feature flagi, parametry systemu - zmiana na jednym węźle, propagacja na cały klaster |
| Rozproszony rate limiting | DeltaCrdt | Token bucket replikowany w klastrze - spójne limity bez centralnego koordynatora |
| Synchronizacja offline | Phoenix.Sync / ElectricSQL | Dane PostgreSQL na urządzeniu mobilnym, praca offline, automatyczna synchronizacja |
| Rozproszone cache'owanie | DeltaCrdt | In-memory cache spójny w klastrze - bez Redisa, bez Memcached |
| Kolaboratywna edycja | Custom CRDT + Phoenix PubSub | Wielu użytkowników edytuje ten sam dokument jednocześnie |
Wzorzec: kolaboratywna edycja w LiveView
defmodule MyAppWeb.DocumentLive do
use MyAppWeb, :live_view
def mount(%{"id" => doc_id}, _session, socket) do
topic = "document:#{doc_id}"
if connected?(socket) do
Phoenix.PubSub.subscribe(MyApp.PubSub, topic)
MyApp.Presence.track(self(), topic, socket.assigns.current_user.id, %{
name: socket.assigns.current_user.name,
cursor: %{line: 0, col: 0},
color: random_color()
})
end
doc = MyApp.Documents.get!(doc_id)
{:ok, assign(socket,
doc: doc,
topic: topic,
presences: MyApp.Presence.list(topic)
)}
end
def handle_event("edit", %{"field" => field, "value" => value}, socket) do
# Zapisz zmianę lokalnie
{:ok, doc} = MyApp.Documents.update_field(socket.assigns.doc, field, value)
# Broadcast operacji (nie stanu) - inne klienty mergują
Phoenix.PubSub.broadcast(MyApp.PubSub, socket.assigns.topic, %{
event: :field_updated,
field: field,
value: value,
user_id: socket.assigns.current_user.id,
timestamp: DateTime.utc_now()
})
{:noreply, assign(socket, doc: doc)}
end
def handle_event("cursor_move", %{"line" => line, "col" => col}, socket) do
MyApp.Presence.update(self(), socket.assigns.topic,
socket.assigns.current_user.id,
fn meta -> %{meta | cursor: %{line: line, col: col}} end
)
{:noreply, socket}
end
def handle_info(%{event: :field_updated, field: field, value: value, user_id: uid}, socket) do
if uid != socket.assigns.current_user.id do
doc = MyApp.Documents.apply_remote_change(socket.assigns.doc, field, value)
{:noreply, assign(socket, doc: doc)}
else
{:noreply, socket}
end
end
def handle_info(%Phoenix.Socket.Broadcast{event: "presence_diff", payload: diff}, socket) do
presences = MyApp.Presence.sync_diff(socket.assigns.presences, diff)
{:noreply, assign(socket, presences: presences)}
end
endPhoenix.Presence (CRDT) śledzi, kto edytuje dokument i gdzie jest kursor. PubSub propaguje operacje edycji. Każdy klient aplikuje zmiany lokalnie. Wynik: Google Docs w Twojej aplikacji Phoenix bez żadnej zewnętrznej infrastruktury.
Kiedy CRDT nie jest odpowiedzią
Uczciwie - nie każdy problem wymaga CRDT.
Silna spójność jest wymagana - systemy transakcyjne, rezerwacje, aukcje. Gdy dwa bilety lotnicze nie mogą być sprzedane na to samo miejsce, potrzebujesz konsensusu, nie CRDT. CRDT gwarantuje eventual consistency - "w końcu będzie spójne". Dla biletu lotniczego "w końcu" jest za późno.
Dane są write-heavy z jednym źródłem - logi, telemetria, strumienie eventów. Gdy jeden producent generuje dane, a wielu konsumentów czyta, CRDT jest overkill. Append-only log wystarczy.
Merge nie ma sensu - nie każdy typ danych da się sensownie zmergować. Co znaczy "zmerguj dwa zamówienia złożone jednocześnie na ten sam produkt"? Połowa + połowa? Dwa osobne? To zależy od logiki biznesowej, nie od struktury danych.
Overhead pamięciowy jest problemem - CRDT przechowuje metadane do śledzenia przyczynowości (vector clocks, dot contexts). Dla milionów drobnych wartości ten overhead może być istotny. G-Counter na 100 węzłach to 100 integerów zamiast jednego.
Przyszłość: Mnesia z CRDT
Warto wspomnieć o inicjatywie, która może zmienić cały ekosystem. Erlang Solutions sponsoruje prace nad dodaniem natywnego rozwiązywania konfliktów opartego na CRDT do Mnesia - rozproszonej bazy danych wbudowanej w OTP.
Mnesia istnieje od lat 90. i jest używana w systemach telekomunikacyjnych na całym świecie. Jej główna słabość: rozwiązywanie konfliktów po partycji sieciowej wymaga ręcznej interwencji. CRDT eliminuje ten problem - konflikty rozwiązują się automatycznie, matematycznie poprawnie.
Jeśli CRDT trafi do Mnesia, każda aplikacja Elixirowa będzie miała dostęp do rozproszonej bazy danych z automatycznym rozwiązywaniem konfliktów bez żadnych zewnętrznych zależności. Zero Redisa, zero PostgreSQL dla danych tymczasowych, zero konfiguracji. Mnesia + CRDT + supervision trees = samoleczący się, rozproszony storage wbudowany w runtime.
Stack CRDT w Elixirze - podsumowanie
| Warstwa | Narzędzie | Typ CRDT | Zakres |
|---|---|---|---|
| Obecność użytkowników | Phoenix.Presence | Delta-state | Wbudowane w Phoenix |
| Rozproszone procesy | Horde | AWLWWMap (DeltaCrdt) | Klaster BEAM |
| Storage klucz-wartość | DeltaCrdt | AWLWWMap, inne | Klaster BEAM |
| Synchronizacja klient-serwer | Phoenix.Sync / ElectricSQL | Column-level merge | Przeglądarka ↔ PostgreSQL |
| Baza rozproszona (przyszłość) | Mnesia + CRDT | W trakcie prac | OTP |
Pięć warstw, jedna filozofia: dane, które same rozwiązują konflikty. Od śledzenia kursorów po synchronizację offline, od konfiguracji runtime po rozproszone procesy.
Elixir nie "obsługuje" CRDT. Elixir żyje CRDT. Phoenix.Presence to CRDT, którego używasz od pierwszego mix phx.new. Horde to CRDT, który zarządza Twoimi procesami. DeltaCrdt to CRDT, który replikuje Twoje dane. A BEAM to runtime, który sprawia, że to wszystko działa naturalnie - bo był projektowany do systemów rozproszonych zanim CRDT miało nazwę.
Budujesz system, w którym wielu użytkowników musi pracować na tych samych danych jednocześnie - online i offline? Porozmawiajmy - pokażemy, jak CRDT w Phoenix rozwiązuje problem, z którym walczysz.