Consistency di distributed system adalah janji tentang data mana yang boleh dibaca, saat datanya punya beberapa salinan di beberapa server. Bayangin kampus dengan papan pengumuman di tiga gedung. Bagian akademik menempel pengumuman baru di gedung pusat, lalu kurir membawa fotokopinya ke gedung lain. Kurir ke gedung A cepat, kurir ke gedung B lambat. Mahasiswa di gedung B masih membaca jadwal lama, padahal di pusat sudah ganti. Papan di gedung pusat itu primary, papan lain replica, dan kurirnya replikasi. Model consistency menjawab: siapa boleh membaca papan yang mana, dan kapan.
🧪 Tebakan cepat: Budi mengganti email akunnya dari
[email protected]ke[email protected], lalu me-reload halaman profil 500 ms kemudian. Read-nya dilayani replica yang tertinggal 1.000 ms. Email mana yang Budi lihat? Tebak dulu, jawabannya ada di bagian "Replica yang tertinggal".
Replica yang tertinggal
Di lesson Database Scaling, TiketNada menambah read replica: salinan database PostgreSQL yang melayani SELECT, sementara semua write tetap ke primary. Replikasinya asynchronous: primary menjawab "sukses" begitu write-nya tersimpan di primary, lalu mengirim perubahannya ke replica belakangan. Jarak waktu sampai replica ikut punya data itu disebut replication lag. Saat war tiket replica sibuk, jadi lag-nya membengkak. Model contoh lesson ini: replica tertinggal 1.000 ms.
| ms | Primary | Replica | Budi reload |
|---|---|---|---|
| 0 | UPDATE: email jadi [email protected] |
masih [email protected] |
|
| 500 | [email protected] |
masih [email protected] |
baca dari replica: [email protected] |
| 1.000 | [email protected] |
menerapkan perubahan: [email protected] |
Jawaban tebakannya: Budi melihat email lamanya sendiri. Dia mengira perubahannya gagal, lalu mengganti lagi, atau menghubungi CS. Data-nya gak rusak; 500 ms lagi replica akan menyusul. Masalahnya ada di janji yang diberikan sistem ke pembacanya.
Strong dan eventual
Dua ujung janjinya:
- Strong consistency: setiap read selalu mengembalikan write terbaru. Caranya, misalnya, semua read ke primary, atau replikasi synchronous (write baru dianggap sukses setelah replica ikut menyimpan). Aman, tapi mahal: primary menanggung semua read, atau setiap write menunggu replica.
- Eventual consistency: kalau gak ada write baru, semua salinan akhirnya sama. Sementara itu read boleh mengembalikan data lama (stale). Murah dan gampang di-scale, karena replica mana pun boleh menjawab.
Kebanyakan data TiketNada aman dengan eventual: sisa kursi di halaman konser telat 1 detik gak merugikan siapa pun. Tapi ada dua kasus yang bikin pengguna bingung, dan dua janji di tengah-tengah untuk mengatasinya.
Read-your-writes
Read-your-writes menjanjikan satu hal: orang yang menulis selalu melihat tulisannya sendiri. Orang lain boleh masih melihat data lama sebentar. Ini cukup untuk Budi, dan gak memaksa semua read ke primary.
Caranya di PostgreSQL: setelah write, aplikasi mencatat posisi log-nya (LSN, Log Sequence Number) di session Budi, dari pg_current_wal_lsn(). Sebelum membaca dari replica, cek apakah replica sudah menerapkan posisi itu:
// read-your-writes: yang baru menulis membaca dari primary,
// sampai replica sudah menerapkan tulisannya
async function sumberBaca(sesi) {
if (!sesi.lsnTerakhir) return replica; // belum pernah menulis
const { rows } = await replica.query(
"SELECT pg_last_wal_replay_lsn() >= $1::pg_lsn AS sudah",
[sesi.lsnTerakhir]
);
return rows[0].sudah ? replica : primary;
}
sesi.lsnTerakhirdiisi saat Budi mengganti email, misalnya0/3000148.pg_last_wal_replay_lsn()adalah posisi terakhir yang sudah diterapkan replica. Kalau sudah melewati tulisan Budi, replica aman dipakai.- Kalau belum, read Budi ke primary. Di ms 500, Budi melihat
[email protected]. Siti dari CS yang membuka profil Budi di detik yang sama masih membaca replica dan melihat[email protected], dan itu boleh.
Cara yang lebih sederhana: setelah menulis, baca dari primary selama beberapa detik (Rails memakai 2 detik secara default untuk ini).
Causal consistency
Kasus kedua butuh dua penulis. Di halaman Konser Nusantara ada diskusi. Database diskusi TiketNada di-shard per penulis (lesson Database Scaling), jadi:
- ms 0: Budi bertanya "Ada parkir motor?". Tersimpan di shard-1, yang replica-nya tertinggal 1.200 ms.
- ms 400: Admin membaca pertanyaan itu, lalu menjawab "Ada, di basement P2". Tersimpan di shard-2, yang replica-nya tertinggal 300 ms.
- ms 800: Siti membuka diskusi, dibaca dari replica.
| Siti membaca di | Pertanyaan Budi (shard-1, tiba ms 1.200) | Jawaban admin (shard-2, tiba ms 700) |
|---|---|---|
| ms 800, eventual | belum ada | "Ada, di basement P2" |
| ms 800, causal | belum ada | ditahan |
| ms 1.200, causal | ada | ada |
Dengan eventual, Siti melihat jawaban untuk pertanyaan yang belum ada. Causal consistency mencegahnya: kalau tulisan B dibuat setelah penulisnya melihat tulisan A (B bergantung pada A), siapa pun yang melihat B juga harus melihat A. Caranya, jawaban admin membawa catatan "aku membalas pertanyaan versi sekian di shard-1", dan jawaban baru ditampilkan setelah pertanyaannya terlihat. Read-your-writes gak menolong Siti di sini, karena Siti sendiri gak menulis apa-apa. Sebaliknya, causal sudah mencakup read-your-writes: read Budi bergantung pada tulisannya sendiri.
| Janji | Yang dijamin | Harga |
|---|---|---|
| Strong | semua read melihat write terbaru | semua read ke primary, atau write menunggu replica |
| Causal | yang bergantung gak muncul sebelum asalnya | mencatat dan memeriksa ketergantungan |
| Read-your-writes | penulis melihat tulisannya sendiri | read penulis kadang ke primary |
| Eventual | akhirnya sama | data lama bisa terbaca sebentar |
Outbox: database dan event gak boleh beda cerita
Masalah consistency yang lain: satu aksi mengubah dua sistem. Saat pesanan #1001 dibayar, order mengubah status di PostgreSQL dan mengirim event order.dibayar ke queue e-tiket (lesson Message Queue). Kalau proses order crash di antara keduanya:
- Update DB dulu, lalu publish: crash setelah
COMMIT. Pesanan dibayar, event gak pernah terkirim, Budi gak pernah dapat e-tiket. - Publish dulu, lalu update DB: crash setelah publish. E-tiket terkirim untuk pesanan yang di database belum dibayar.
Outbox pattern menyelesaikannya: event ditulis sebagai baris di tabel outbox, di transaksi database yang sama dengan perubahan datanya. Keduanya tersimpan atau keduanya batal. Proses terpisah (relay) membaca tabel itu dan mem-publish ke queue.
// order: tandai #1001 dibayar DAN catat event-nya, dalam SATU transaksi
const client = await pool.connect();
try {
await client.query("BEGIN");
await client.query("UPDATE orders SET status = 'dibayar' WHERE id = $1", [1001]);
await client.query(
"INSERT INTO outbox (topic, payload) VALUES ($1, $2)",
["order.dibayar", JSON.stringify({ pesanan: 1001 })]
);
await client.query("COMMIT");
} catch (err) {
await client.query("ROLLBACK");
throw err;
} finally {
client.release();
}
// relay: proses terpisah, jalan tiap 1 detik
async function kirimOutbox() {
const { rows } = await pool.query(
"SELECT id, topic, payload FROM outbox WHERE sent_at IS NULL ORDER BY id LIMIT 100"
);
for (const row of rows) {
await queue.publish(row.topic, row.payload);
await pool.query("UPDATE outbox SET sent_at = now() WHERE id = $1", [row.id]);
}
}
setInterval(kirimOutbox, 1000);
BEGINsampaiCOMMIT: update pesanan dan baris outbox masuk bersama. Crash di tengah berarti dua-duanya gak ada.- Relay mencari baris yang
sent_at-nya masih kosong, mem-publish, lalu menandainya terkirim. Crash setelahCOMMITgak masalah: barisnya menunggu di tabel, dan relay mengirimnya di detik berikutnya. Di model Lab: commit di ms 6, relay mem-publish di ms 1.000, e-tiket terkirim di ms 1.500. - Kalau relay crash setelah publish tapi sebelum
UPDATE ... sent_at, event-nya terkirim dua kali. Jadi consumer harus idempotent (lesson Message Queue). Outbox menjamin event paling sedikit sekali, bukan tepat sekali.
Daripada polling tiap detik, relay juga bisa membaca log PostgreSQL langsung (CDC, Change Data Capture, misalnya dengan Debezium), jadi event terkirim lebih cepat.
Konsistensi lintas service: sekilas Saga
Outbox menjaga satu service tetap jujur. Kalau satu aksi butuh perubahan di beberapa service (pesan kursi di tiket, potong kartu di bayar), dipakai Saga: urutan transaksi lokal di tiap service, dan kalau satu langkah gagal, langkah yang sudah jalan dibatalkan dengan compensating transaction (misalnya kursi dilepas lagi). Kenapa gak satu transaksi besar saja dibahas di lesson Distributed Transactions, dan Saga dibedah di lesson Saga Pattern.
⚠️ Jebakan yang sering ketemu
- Membaca dari replica tepat setelah menulis. Pengguna melihat perubahannya "hilang". Pakai read-your-writes untuk halaman milik si penulis.
- Menjadikan semua read strong "biar aman". Primary menanggung semua beban, dan replica jadi percuma. Pilih janji per jenis data.
- Update database dan publish event sebagai dua langkah terpisah. Crash di antaranya bikin keduanya beda cerita. Pakai outbox.
- Consumer yang gak idempotent di belakang outbox. Outbox bisa mengirim event yang sama dua kali; e-tiket Budi jangan sampai terkirim dobel.
TL;DR: Saat data punya beberapa salinan, model consistency menentukan apa yang boleh terbaca. Replikasi asynchronous membuat replica tertinggal (replication lag, di model Lab 1.000 ms), jadi Budi bisa melihat email lamanya sendiri setelah mengganti. Strong consistency: setiap read mengembalikan data terbaru, mahal karena butuh koordinasi. Eventual consistency: data akhirnya sama, tapi sementara bisa stale, murah dan scalable. Read-your-writes: penulis selalu melihat tulisannya sendiri (cek LSN replica, atau baca dari primary sebentar). Causal: tulisan yang bergantung gak boleh muncul sebelum asalnya (jawaban sebelum pertanyaan). Outbox pattern: simpan event di tabel outbox dalam transaksi yang sama dengan datanya, lalu relay (polling atau CDC) mem-publish ke message broker; event terkirim paling sedikit sekali, jadi consumer harus idempotent. Saga: urutan transaksi lokal per service dengan compensating transaction kalau ada langkah yang gagal.