Distributed transaction adalah satu transaksi yang mengubah data di lebih dari satu database atau service, dan harus berhasil semua atau batal semua. Bayangin kamu mengajak empat teman beli tiket konser bareng, dengan syarat semua ikut atau gak ada yang ikut. Kamu jadi koordinator dan tanya satu-satu: "Siap bayar?" Yang menjawab siap harus menyisihkan uangnya dan gak boleh memakainya untuk hal lain. Setelah semua siap, kamu bilang "bayar sekarang!". Sekarang bayangin HP-mu mati tepat setelah semua menjawab siap. Teman-temanmu menunggu dengan uang tertahan: gak bisa bayar sendiri (siapa tahu ada yang batal), gak bisa memakai uangnya (siapa tahu kamu sudah bilang bayar ke yang lain). Itulah two-phase commit, lengkap dengan kelemahannya.
🧪 Tebakan cepat: Coordinator crash tepat setelah database
tiketdan databasebayarsama-sama menjawab YES. Kursi A-12 jadi apa: terjual, kosong lagi, atau terkunci? Tebak dulu, jawabannya ada di bagian "Coordinator crash: semua menunggu".
Kenapa satu transaksi gak cukup
Di satu database, transaksi itu ACID: semua perintah di antara BEGIN dan COMMIT berhasil bersama atau batal bersama (lesson Transactions & Isolation Levels di track Database). Masalahnya, TiketNada sekarang microservices, dan tiap service punya database sendiri:
tiketmemegang tabelseatsdi db-tiket.bayarmemegang tabelchargesdi db-bayar.
Budi memesan kursi A-12 dan A-13 untuk Konser Nusantara (event #42), pesanan #1001, Rp1.500.000. Dua perubahan, dua database. BEGIN di db-tiket gak bisa melihat db-bayar. Kalau kursi sudah ditandai terjual lalu pemotongan kartu gagal, atau kartu terpotong lalu update kursi gagal, datanya setengah jadi. Distributed transaction berusaha membuat dua database itu berperilaku seperti satu.
Two-phase commit, langkah demi langkah
Two-phase commit (2PC) adalah protokol dengan satu coordinator (di sini service order) dan beberapa participant (db-tiket dan db-bayar). PostgreSQL punya perintahnya sendiri untuk ini:
| Langkah | Coordinator (order) |
db-tiket | db-bayar |
|---|---|---|---|
| 1. Kerja | kirim perintahnya | BEGIN; UPDATE seats ... → UPDATE 2, kursi A-12 dan A-13 terkunci |
BEGIN; INSERT INTO charges ... → INSERT 0 1 |
| 2. Fase 1: prepare | tanya "siap commit?" | PREPARE TRANSACTION 'ord-1001': vote YES |
PREPARE TRANSACTION 'ord-1001': vote YES |
| 3. Keputusan | tulis ord-1001: COMMIT ke log-nya |
menunggu, kunci tetap dipegang | menunggu |
| 4. Fase 2: commit | kirim "commit!" | COMMIT PREPARED 'ord-1001', kunci lepas |
COMMIT PREPARED 'ord-1001' |
- Fase 1 (prepare/voting): tiap participant menyimpan perubahannya ke disk dalam keadaan "siap", tetap memegang kuncinya, lalu menjawab YES. YES adalah janji: "apa pun yang terjadi, kalau disuruh commit, aku pasti bisa commit".
- Fase 2 (commit/rollback): kalau semua YES, coordinator memerintahkan commit. Kalau ada satu saja NO (misalnya kartu Budi ditolak, jadi db-bayar menjawab
ROLLBACK), coordinator memerintahkanROLLBACK PREPAREDke yang sudah siap, dan kursinya kosong lagi.
Ini coordinator-nya dalam JavaScript:
// order sebagai coordinator: fase 1, keputusan, fase 2
async function twoPhaseCommit(gid, peserta) {
const suara = [];
for (const p of peserta) suara.push(await p.prepare(gid)); // "YES" atau "NO"
const hasil = suara.every((s) => s === "YES") ? "COMMIT" : "ABORT";
await logKeputusan.tulis(gid, hasil); // disimpan permanen SEBELUM dikirim
for (const p of peserta) {
if (hasil === "COMMIT") await p.commit(gid); // COMMIT PREPARED 'ord-1001'
else await p.rollback(gid); // ROLLBACK PREPARED 'ord-1001'
}
return hasil;
}
p.prepare(gid)menjalankanPREPARE TRANSACTIONdi participant itu dan mengembalikan suaranya.logKeputusan.tulismenyimpan keputusan ke disk dulu. Kalau coordinator crash lalu pulih, log ini yang memberi tahu harus commit atau rollback.- Satu NO saja membuat hasilnya
ABORTuntuk semua.
Coordinator crash: semua menunggu
Sekarang order crash setelah dua-duanya vote YES, sebelum menulis keputusan. Model contoh Lab:
| Waktu | Yang terjadi |
|---|---|
| 10:00:00 | db-tiket dan db-bayar vote YES. Kursi A-12 dan A-13 terkunci. |
| 10:00:00 | order crash. Gak ada keputusan di log-nya. |
| 10:00:30 | Siti mencoba membeli A-12: ERROR: canceling statement due to lock timeout |
| 10:05:00 | order pulih, gak menemukan keputusan, lalu mengirim ROLLBACK PREPARED. Kursi lepas. |
Jawaban tebakannya: kursi A-12 terkunci, bukan terjual dan bukan kosong, selama 5 menit, di tengah war tiket. db-tiket gak boleh memutuskan sendiri. Kalau dia rollback, mungkin coordinator sudah memutuskan COMMIT dan db-bayar sudah memotong kartu Budi: uang terpotong, kursi lepas. Kalau dia commit, mungkin db-bayar tadinya bilang NO. Participant yang sudah vote YES cuma bisa menunggu. Itulah kenapa 2PC disebut protokol blocking: satu coordinator yang mati membekukan semua participant-nya.
Saat pulih, coordinator membaca log-nya. Ada COMMIT di log? Kirim COMMIT PREPARED. Gak ada keputusan? Anggap batal (presumed abort) dan kirim ROLLBACK PREPARED. Di PostgreSQL, transaksi yang menggantung begini terlihat di view pg_prepared_xacts.
Kenapa kebanyakan sistem menghindari 2PC
- Blocking. Coordinator jadi single point of failure: dia mati, kunci di semua participant ikut membeku.
- Kunci dipegang lebih lama. Di transaksi lokal, kursi terkunci sekitar 5 ms. Dengan 2PC, kuncinya dipegang selama dua putaran jaringan tambahan, di model contoh sekitar 50 ms. Kursi paling diincar saat war tiket cuma bisa berpindah tangan 20 kali per detik, bukan 200.
- Semua participant harus mendukungnya. PostgreSQL bahkan mematikannya secara default (
max_prepared_transactions = 0);PREPARE TRANSACTIONlangsung dijawabERROR: prepared transactions are disabled. Payment gateway, API pihak ketiga, dan kebanyakan message broker gak punya fase "prepare" sama sekali. - Service jadi saling mengikat.
tiketgak bisa melepas kursinya sampaibayardanordersama-sama sehat.
2PC tetap dipakai di tempat yang terkendali, misalnya di dalam satu database terdistribusi atau antara dua database milik satu tim, dengan volume kecil. Lintas microservice, pilihannya hampir selalu yang berikut.
Alternatifnya: Saga
Saga menukar satu transaksi besar dengan urutan transaksi lokal, masing-masing langsung commit di database-nya sendiri. Kalau satu langkah gagal, langkah yang sudah jalan dibatalkan dengan compensating transaction:
tiketmenandai A-12 dan A-13 terjual (commit lokal).bayarmemotong kartu. Ditolak? Jalankan kompensasi langkah 1:tiketmelepas kursinya lagi.
Gak ada kunci yang dipegang melintasi jaringan, jadi gak ada yang membeku. Harganya: ada saat sebentar ketika kursi tercatat terjual padahal pembayarannya belum jelas (eventual consistency), dan tiap langkah butuh kompensasinya sendiri. Ada dua cara mengatur urutannya:
- Choreography: tiap service mendengarkan event dari service sebelumnya dan bereaksi sendiri.
tiketmem-publishkursi.dipesan,bayarmendengarnya lalu memotong kartu, lalu mem-publishpembayaran.gagalataupembayaran.sukses. Gak ada pusatnya. - Orchestration: satu orchestrator (misalnya
order) memerintah tiap service secara berurutan dan menjalankan kompensasi kalau ada yang gagal.
Cara kerjanya, kompensasi yang aman diulang, dan kapan memilih yang mana dibedah di lesson berikutnya, Saga Pattern.
⚠️ Jebakan yang sering ketemu
- Mengira 2PC menjamin "tanpa masalah". 2PC menjamin atomik, bukan tersedia. Coordinator mati berarti kunci membeku.
- Coordinator yang gak menulis keputusannya ke disk sebelum mengirim. Setelah crash, dia gak tahu lagi harus commit atau rollback.
- Participant yang memutuskan sendiri setelah vote YES. Rollback sepihak bisa membuat kartu terpotong tanpa kursi.
- Memakai 2PC antar microservice yang dipanggil ribuan kali per detik. Kunci yang dipegang lebih lama menurunkan throughput tepat saat paling dibutuhkan.
TL;DR: Distributed transaction mengubah data di beberapa database atau service sebagai satu kesatuan. Two-phase commit (2PC) memakai coordinator: fase 1, coordinator meminta semua participant vote (PREPARE TRANSACTION, YES berarti janji pasti bisa commit, kunci tetap dipegang); fase 2, kalau semua YES coordinator memerintahkan commit (COMMIT PREPARED), kalau ada satu NO semuanya rollback (ROLLBACK PREPARED). Keputusannya ditulis ke log coordinator sebelum dikirim. Kelemahan utamanya: 2PC adalah protokol blocking. Kalau coordinator crash di antara dua fase, participant yang sudah vote YES terkunci sampai coordinator pulih (di Lab: kursi A-12 terkunci 5 menit, Siti kena lock timeout). Karena itu, ditambah kunci yang lebih lama dan participant yang sering gak mendukung prepare, kebanyakan sistem menghindari 2PC lintas service dan memakai Saga: urutan transaksi lokal dengan compensating transaction. Di Saga choreography, tiap service mendengarkan event service sebelumnya dan bereaksi; di orchestration, satu orchestrator memerintah urutannya.