Distributed Transactions — System Design

Distributed Transactions adalah transaksi yang melibatkan lebih dari satu database atau service. Ini adalah salah satu masalah tersulit dalam system design…

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 tiket dan database bayar sama-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:

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'

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;
}

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

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:

  1. tiket menandai A-12 dan A-13 terjual (commit lokal).
  2. bayar memotong kartu. Ditolak? Jalankan kompensasi langkah 1: tiket melepas 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:

Cara kerjanya, kompensasi yang aman diulang, dan kapan memilih yang mana dibedah di lesson berikutnya, Saga Pattern.

⚠️ Jebakan yang sering ketemu

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.

Yang akan kamu pelajari