1. Gambaran keseluruhan
Dalam artikel ini, kita akan melihat jenis data yang direplikasi tanpa konflik (CRDT) dan cara bekerja dengannya di Java. Sebagai contoh, kami akan menggunakan pelaksanaan dari perpustakaan wurmloch-crdt .
Apabila kita mempunyai sekumpulan N replika N dalam sistem yang diedarkan, kita mungkin menghadapi partisi rangkaian - beberapa node tidak dapat berkomunikasi antara satu sama lain buat sementara waktu . Keadaan ini disebut otak berpecah.
Apabila kita mempunyai otak yang berasingan dalam sistem kita, beberapa permintaan menulis - walaupun untuk pengguna yang sama - dapat pergi ke replika yang berbeza yang tidak saling berhubungan . Apabila keadaan seperti itu berlaku, sistem kami masih tersedia tetapi tidak konsisten .
Kita perlu memutuskan apa yang harus dilakukan dengan penulisan dan data yang tidak konsisten apabila rangkaian antara dua kluster perpecahan mula berfungsi semula.
2. Jenis Data Replikasi Bebas Konflik kepada Penyelamat
Mari kita pertimbangkan dua nod, A dan B , yang telah terputus kerana otak yang berpecah.
Katakan pengguna menukar login dan bahawa permintaan pergi ke nod A . Maka dia / dia memutuskan untuk menukarnya lagi, tetapi kali ini permintaan pergi ke nod B .
Kerana otak terbelah, kedua-dua nod tidak bersambung. Kita perlu memutuskan bagaimana log masuk pengguna ini akan kelihatan ketika rangkaian berfungsi semula.
Kami dapat menggunakan beberapa strategi: kami dapat memberi peluang untuk menyelesaikan konflik kepada pengguna (seperti yang dilakukan di Google Docs), atau kami dapat menggunakan CRDT untuk menggabungkan data dari replika yang berbeda untuk kami.
3. Ketergantungan Maven
Pertama, mari tambahkan kebergantungan ke perpustakaan yang menyediakan sekumpulan CRDT berguna:
com.netopyr.wurmloch wurmloch-crdt 0.1.0
Versi terbaru boleh didapati di Maven Central.
4. Set Grow-Only
CRDT yang paling asas adalah Set Grow-Only. Elemen hanya boleh ditambahkan ke GSet dan tidak pernah dikeluarkan. Apabila GSet menyimpang, ia dapat digabungkan dengan mudah dengan mengira penyatuan dua set.
Pertama, mari buat dua replika untuk mensimulasikan struktur data yang diedarkan dan menghubungkan kedua-dua replika tersebut menggunakan kaedah connect () :
LocalCrdtStore crdtStore1 = new LocalCrdtStore(); LocalCrdtStore crdtStore2 = new LocalCrdtStore(); crdtStore1.connect(crdtStore2);
Sebaik sahaja kami mendapat dua replika dalam kluster kami, kami dapat membuat GSet pada replika pertama dan merujuknya pada replika kedua:
GSet replica1 = crdtStore1.createGSet("ID_1"); GSet replica2 = crdtStore2.findGSet("ID_1").get();
Pada ketika ini, kluster kami berfungsi seperti yang diharapkan, dan terdapat hubungan aktif antara dua replika. Kami dapat menambahkan dua elemen pada set dari dua replika yang berbeza dan menegaskan bahawa set tersebut mengandungi elemen yang sama pada kedua replika:
replica1.add("apple"); replica2.add("banana"); assertThat(replica1).contains("apple", "banana"); assertThat(replica2).contains("apple", "banana");
Katakan secara tiba-tiba kita mempunyai partisi rangkaian dan tidak ada hubungan antara replika pertama dan kedua. Kita boleh mensimulasikan partisi rangkaian menggunakan kaedah putuskan () :
crdtStore1.disconnect(crdtStore2);
Seterusnya, apabila kita menambahkan elemen ke kumpulan data dari kedua replika, perubahan tersebut tidak dapat dilihat secara global kerana tidak ada hubungan antara mereka:
replica1.add("strawberry"); replica2.add("pear"); assertThat(replica1).contains("apple", "banana", "strawberry"); assertThat(replica2).contains("apple", "banana", "pear");
Setelah hubungan antara kedua-dua anggota kluster terjalin semula, GSet digabungkan secara dalaman menggunakan penyatuan pada kedua set, dan kedua-dua replika itu kembali konsisten:
crdtStore1.connect(crdtStore2); assertThat(replica1) .contains("apple", "banana", "strawberry", "pear"); assertThat(replica2) .contains("apple", "banana", "strawberry", "pear");
5. Kaunter Kenaikan Kenaikan
Counter Increment-Only adalah CRDT yang mengumpulkan semua kenaikan secara tempatan pada setiap nod.
Apabila replika diselaraskan, selepas partisi rangkaian, nilai yang dihasilkan dikira dengan menjumlahkan semua kenaikan pada semua nod - ini serupa dengan LongAdder dari java.concurrent tetapi pada tahap abstraksi yang lebih tinggi.
Mari buat penghitung peningkatan hanya menggunakan GCounter dan tingkatkannya dari kedua replika. Kita dapat melihat bahawa jumlahnya dikira dengan betul:
LocalCrdtStore crdtStore1 = new LocalCrdtStore(); LocalCrdtStore crdtStore2 = new LocalCrdtStore(); crdtStore1.connect(crdtStore2); GCounter replica1 = crdtStore1.createGCounter("ID_1"); GCounter replica2 = crdtStore2.findGCounter("ID_1").get(); replica1.increment(); replica2.increment(2L); assertThat(replica1.get()).isEqualTo(3L); assertThat(replica2.get()).isEqualTo(3L);
Apabila kita memutuskan kedua-dua anggota kluster dan melakukan operasi kenaikan tempatan, kita dapat melihat bahawa nilainya tidak konsisten:
crdtStore1.disconnect(crdtStore2); replica1.increment(3L); replica2.increment(5L); assertThat(replica1.get()).isEqualTo(6L); assertThat(replica2.get()).isEqualTo(8L);
Tetapi setelah kluster kembali sihat, kenaikan akan digabungkan, menghasilkan nilai yang tepat:
crdtStore1.connect(crdtStore2); assertThat(replica1.get()) .isEqualTo(11L); assertThat(replica2.get()) .isEqualTo(11L);
6. Kaunter PN
Dengan menggunakan peraturan yang serupa untuk pembilang kenaikan hanya, kita dapat membuat pembilang yang dapat dinaikkan dan dikurangkan. The PNCounter menyimpan semua kenaikan dan pengurangan secara berasingan.
Apabila replika diselaraskan, nilai yang dihasilkan akan sama dengan jumlah semua kenaikan tolak jumlah semua penurunan :
@Test public void givenPNCounter_whenReplicasDiverge_thenMergesWithoutConflict() { LocalCrdtStore crdtStore1 = new LocalCrdtStore(); LocalCrdtStore crdtStore2 = new LocalCrdtStore(); crdtStore1.connect(crdtStore2); PNCounter replica1 = crdtStore1.createPNCounter("ID_1"); PNCounter replica2 = crdtStore2.findPNCounter("ID_1").get(); replica1.increment(); replica2.decrement(2L); assertThat(replica1.get()).isEqualTo(-1L); assertThat(replica2.get()).isEqualTo(-1L); crdtStore1.disconnect(crdtStore2); replica1.decrement(3L); replica2.increment(5L); assertThat(replica1.get()).isEqualTo(-4L); assertThat(replica2.get()).isEqualTo(4L); crdtStore1.connect(crdtStore2); assertThat(replica1.get()).isEqualTo(1L); assertThat(replica2.get()).isEqualTo(1L); }
7. Daftar Terakhir Penulis-Menang
Sometimes, we have more complex business rules, and operating on sets or counters is insufficient. We can use the Last-Writer-Wins Register, which keeps only the last updated value when merging diverged data sets. Cassandra uses this strategy to resolve conflicts.
We need to be very cautious when using this strategy because it drops changes that occurred in the meantime.
Let's create a cluster of two replicas and instances of the LWWRegister class:
LocalCrdtStore crdtStore1 = new LocalCrdtStore("N_1"); LocalCrdtStore crdtStore2 = new LocalCrdtStore("N_2"); crdtStore1.connect(crdtStore2); LWWRegister replica1 = crdtStore1.createLWWRegister("ID_1"); LWWRegister replica2 = crdtStore2.findLWWRegister("ID_1").get(); replica1.set("apple"); replica2.set("banana"); assertThat(replica1.get()).isEqualTo("banana"); assertThat(replica2.get()).isEqualTo("banana");
When the first replica sets the value to apple and the second one changes it to banana, the LWWRegister keeps only the last value.
Let's see what happens if the cluster disconnects:
crdtStore1.disconnect(crdtStore2); replica1.set("strawberry"); replica2.set("pear"); assertThat(replica1.get()).isEqualTo("strawberry"); assertThat(replica2.get()).isEqualTo("pear");
Each replica keeps its local copy of data that is inconsistent. When we call the set() method, the LWWRegister internally assigns a special version value that identifies the specific update to every using a VectorClock algorithm.
When the cluster synchronizes, it takes the value with the highest versionanddiscards every previous update:
crdtStore1.connect(crdtStore2); assertThat(replica1.get()).isEqualTo("pear"); assertThat(replica2.get()).isEqualTo("pear");
8. Conclusion
In this article, we showed the problem of consistency of distributed systems while maintaining availability.
In case of network partitions, we need to merge the diverged data when the cluster is synchronized. We saw how to use CRDTs to perform a merge of diverged data.
Semua contoh dan coretan kod ini boleh didapati di projek GitHub - ini adalah projek Maven, jadi mudah diimport dan dijalankan sebagaimana adanya.