Jawaban singkat untuk pertanyaan yang paling sering diajukan pembaca tentang topik ini.
01Apa perbedaan two-phase commit dan saga?
Two-phase commit mengoordinasikan satu commit atomic lintas participant: semua prepare dan menahan lock, lalu semua commit atau semua rollback. Saga meng-commit setiap langkah secara lokal dan, jika langkah berikutnya gagal, menjalankan compensating action untuk membatalkan langkah sebelumnya. 2PC memberi isolation tetapi bisa memblokir; saga tetap available tetapi memperlihatkan state perantara.
02Apakah PostgreSQL mendukung two-phase commit?
Ya, lewat PREPARE TRANSACTION, COMMIT PREPARED, dan ROLLBACK PREPARED, dan session mana pun bisa menyelesaikan prepared transaction. Fiturnya nonaktif secara default karena max_prepared_transactions bernilai 0, dan mengubahnya butuh restart server. Dokumentasi menyebut fitur ini ditujukan untuk external transaction manager, bukan untuk kode aplikasi biasa.
03Mengapa two-phase commit disebut protokol yang memblokir?
Setelah participant memberi vote yes, ia tidak bisa commit atau rollback sendiri, karena participant lain mungkin sudah diberi perintah sebaliknya. Ia harus menunggu keputusan coordinator sambil tetap menahan lock. Jika coordinator gagal permanen, participant itu tetap in-doubt dan row yang terkunci tidak bisa ditulis.
04Apakah saga bisa menjamin isolation?
Tidak. Setiap langkah langsung di-commit, sehingga transaksi lain bisa melihat state perantara sebelum compensation berjalan. Dampaknya bisa dikurangi dengan semantic lock seperti status PENDING, commutative update, dan membaca ulang nilai sebelum menulis, tetapi isolation seperti satu database transaction tidak bisa didapat.
05Kapan sebaiknya memakai saga daripada two-phase commit?
Pakai saga bila langkah melintasi service, vendor, atau HTTP API yang tidak bisa prepare, seperti payment gateway, atau bila prosesnya cukup lama sehingga menahan lock tidak dapat diterima. Pakai 2PC hanya bila Anda mengendalikan semua participant, semuanya mendukung prepare, dan transaksinya singkat. Jika seluruh data muat di satu database, transaksi biasa mengalahkan keduanya.
Two-Phase Commit vs Saga: Distributed Transaction Dibandingkan
Two-phase commit vs saga untuk distributed transaction: bagaimana 2PC memblokir dan menahan lock, apa yang ditawarkan PREPARE TRANSACTION, dan kapan saga lebih cocok.
Pakai two-phase commit bila semua participant mendukung prepare, Anda mengoperasikan semuanya, dan transaksinya singkat: atomicity all-or-nothing, tetapi memblokir dan menahan lock jika coordinator mati. Pakai saga bila langkah melintasi service atau vendor: tiap langkah di-commit lokal dan kegagalan memicu compensating action, mengorbankan isolation demi availability.
ERP carwash seperti Qilap menerima pembayaran, mengurangi stok shampoo dan wax, lalu menandai tiket cuci sebagai lunas. Di satu database Postgres itu cukup satu BEGIN dan satu COMMIT, tanpa perlu dipikirkan. Begitu pembayaran ada di gateway dan stok ada di service lain, kalimat yang sama berubah menjadi distributed transaction, dan pertanyaan sebenarnya adalah apa yang terjadi ketika satu sisi berhasil dan sisi lain gagal.
Post ini membandingkan dua jawaban standar, two-phase commit dan saga pattern, dengan contoh checkout tiga langkah. Rujukannya adalah dokumentasi PostgreSQL, artikel Wikipedia tentang two-phase commit, microservices.io, dan paper Sagas yang asli. Implementasi saga di NestJS ada di post saya sebelumnya; post ini fokus pada cara memilih di antara keduanya.
Bagaimana two-phase commit bekerja?
Seorang coordinator memecah satu distributed commit menjadi dua fase. Di fase prepare, ia bertanya ke setiap participant apakah bisa commit; masing-masing membuat perubahannya durable, tetap memegang lock, lalu memberi suara yes atau no. Di fase commit, bila semua suara yes, coordinator menyuruh semuanya commit; bila ada suara no atau suara yang tak pernah datang, semuanya disuruh rollback.
// Two-phase commit, coordinator side. Three participants: orders-db, stock-db, payments-ledger.
// Phase 1: prepare. Each participant makes its work durable, KEEPS its locks, and votes.
votes = [orders.prepare(txid), stock.prepare(txid), payments.prepare(txid)] // 3 requests + 3 votes
log.write(txid, allYes(votes) ? "COMMIT" : "ABORT") // the decision must survive a coordinator crash
// Phase 2: the decision is delivered. Locks are released only when this arrives.
for (p of participants) p.finish(txid, decision) // 3 requests + 3 acks
Contoh hitungan dengan tiga participant: database orders, database stok, dan ledger pembayaran. Prepare butuh 3 request dan 3 vote, keputusan butuh 3 request dan 3 acknowledgement, jadi satu checkout adalah 3 + 3 + 3 + 3 = 12 pesan, ditambah coordinator menulis keputusannya ke log di antara kedua fase. Angkanya kurang penting dibanding jedanya: participant memegang lock sejak memberi vote yes sampai keputusan tiba, dan jeda itulah harga seluruh protokol.
Apa yang terjadi saat coordinator two-phase commit crash?
Two-phase commit adalah protokol yang memblokir. Wikipedia menyebutnya kelemahan terbesar: participant yang sudah memberi vote yes harus menunggu coordinator, dan bila coordinator gagal permanen, sebagian participant tidak akan pernah menyelesaikan transaksinya.
Participant dalam kondisi itu tidak bisa memutuskan sendiri, sebab yang lain mungkin sudah diberi perintah sebaliknya. Maka setiap row yang dikunci tetap terkunci. Jika coordinator mati 5 menit, setiap row yang disentuh checkout in-doubt tidak bisa ditulis selama 5 x 60 = 300 detik, dan setiap order lain yang butuh row stok yang sama mengantre di belakangnya. Satu proses mati menjadi antrean lock di beberapa database, dan itulah harga sebenarnya dari 2PC.
Prepared transaction sengaja bertahan melewati restart, jadi tidak ada yang meng-timeout-kannya untuk Anda. Harus selalu ada seseorang atau sesuatu yang menyelesaikannya dengan commit atau rollback.
Apakah PostgreSQL bisa melakukan two-phase commit?
Bisa, lewat PREPARE TRANSACTION, COMMIT PREPARED, dan ROLLBACK PREPARED. Dokumentasi menyebut prepared transaction disimpan penuh di disk dan bisa di-commit dari session mana pun, bukan hanya session yang membuatnya. Fiturnya mati secara default: max_prepared_transactions bernilai default 0 dan hanya bisa diatur saat server start, jadi mengaktifkannya butuh restart.
-- postgresql.conf (restart required, the default is 0 = feature disabled)
-- max_prepared_transactions = 10
-- Session 1: the stock database's half of the checkout
BEGIN;
UPDATE stock SET qty = qty - 2 WHERE sku = 'WAX-500' AND qty >= 2;
PREPARE TRANSACTION 'checkout-42-stock'; -- no longer tied to this session; state is on disk
-- Any session, even after a crash and restart: what is in doubt right now?
SELECT gid, prepared, owner, database FROM pg_prepared_xacts;
-- The coordinator saw every vote come back yes, so it finishes (from ANY session):
COMMIT PREPARED 'checkout-42-stock';
-- ...or, if any other participant voted no:
-- ROLLBACK PREPARED 'checkout-42-stock';
-- Until one of those runs, the UPDATE above still holds its row lock on WAX-500.
Halaman dokumentasi yang sama tegas bahwa PREPARE TRANSACTION tidak ditujukan untuk aplikasi. Fitur ini ada untuk external transaction manager, dan X/Open XA disebut sebagai contoh standarnya. Dokumentasi juga memperingatkan bahwa prepared transaction yang dibiarkan lama tetap memegang lock, mengganggu VACUUM, dan dalam kasus ekstrem bisa membuat database shutdown demi mencegah transaction ID wraparound. Tanpa transaction manager yang menutupnya dengan cepat, dokumentasi menyarankan membiarkan max_prepared_transactions di nol.
Jika tetap mengaktifkannya, dokumentasi menyarankan nilai minimal sebesar max_connections agar tiap session bisa punya satu prepared transaction yang menunggu. Tambahan dari saya: pasang alert untuk setiap row di pg_prepared_xacts yang lebih tua dari semenit, karena coordinator yang sehat selesai dalam hitungan milidetik.
Apa itu saga, dan apa beda choreography dengan orchestration?
Saga mengganti satu distributed transaction dengan serangkaian local transaction, masing-masing langsung di-commit di service-nya sendiri. Jika sebuah langkah gagal karena aturan bisnis, saga menjalankan compensating transaction yang membatalkan langkah-langkah sebelumnya, seperti dijelaskan microservices.io. Idenya berasal dari paper Sagas tahun 1987 oleh Garcia-Molina dan Salem, yang ditulis untuk long lived transaction yang kalau tidak akan menahan lock berjam-jam atau berhari-hari. Ada dua cara mengoordinasikan langkahnya.
Choreography: setiap local transaction mempublikasikan domain event yang memicu local transaction berikutnya di service lain. Tidak ada komponen pusat, tetapi alurnya tersebar di banyak service dan lebih sulit dibaca.
Orchestration: sebuah orchestrator memberi tahu setiap participant local transaction mana yang dijalankan. Urutan, retry, dan jalur kegagalan ada di satu tempat, dengan harga satu komponen tambahan untuk dijalankan.
Untuk checkout tiga langkah saya memilih orchestration, karena jalur kegagalan adalah bagian yang perlu dibaca dan diuji. Choreography cocok untuk dua atau tiga service dengan alur yang jarang berubah. Implementasi NestJS ada di post saya sebelumnya: Saga pattern untuk distributed transaction di NestJS
Bagaimana compensating action bekerja di saga?
Setiap langkah membawa undo-nya sendiri, dan undo itu adalah transaksi forward yang baru, bukan rollback. Ia tidak bisa menghapus fakta bahwa pembayaran pernah di-authorise; ia hanya bisa melakukan void atau refund. Karena itu compensation harus idempotent dan di-retry sampai berhasil: gangguan yang menggagalkan langkah itu mungkin masih berlangsung. Mekanismenya dibahas di post idempotency keys. Sketsa di bawah menyimpan daftar langkah yang selesai lalu membongkarnya dari belakang.
type Step<C> = {
name: string;
run: (ctx: C) => Promise<void>;
compensate: (ctx: C) => Promise<void>; // a NEW forward action: void, refund, release. Not a rollback.
};
const checkout: Step<Ctx>[] = [
{ name: "reserve-stock", run: reserveStock, compensate: releaseStock },
{ name: "authorise-payment", run: authorisePayment, compensate: voidAuthorisation },
{ name: "confirm-order", run: confirmOrder, compensate: cancelOrder },
];
async function runSaga(steps: Step<Ctx>[], ctx: Ctx) {
const done: Step<Ctx>[] = [];
for (const step of steps) {
try {
await step.run(ctx);
done.push(step); // persist this list: a crash mid-saga must be resumable
} catch (err) {
// Undo in reverse order. Retry each compensation until it succeeds, and make it idempotent,
// because the outage that failed the step may still be going on.
for (const prev of done.reverse()) await retryForever(() => prev.compensate(ctx));
throw err;
}
}
}
Contoh kegagalan: tiga langkah, reserve stock, authorise payment, confirm order. Jika confirm order melempar error, saga menjalankan dua compensation secara terbalik, void authorisation lalu release stock. Kasus terburuknya 3 + 2 = 5 local transaction, dan tak satu pun menahan lock melintasi network call. Urutkan langkah supaya yang tidak bisa dibatalkan, seperti mengirim receipt, berjalan paling akhir. Itu aturan praktis saya, bukan teorema.
Apa yang dikorbankan saga? Masalah semantic isolation
Saga hanya atomic secara eventual, dan tidak punya isolation. Paper Sagas menggambarkan rangkaian transaksi yang bisa saling berselang-seling dengan pekerjaan lain, sehingga transaksi lain melihat state perantara. Di antara langkah 1 dan sebuah compensation, stok yang di-reserve terlihat oleh semua orang.
Contoh hitungan: stok 10 unit. Saga A me-reserve 6, tersisa 4. Saga B meminta 5, melihat 4, dan diberi tahu stok habis. Lalu pembayaran A gagal dan compensation-nya mengembalikan 6 unit, 4 + 6 = 10, yang sebenarnya cukup untuk B. B ditolak berdasarkan state yang tidak pernah menjadi nyata. Tiga countermeasure umum mengurangi dampaknya.
Semantic lock: tandai row dengan status seperti PENDING supaya alur lain tahu nilainya masih sementara dan bisa menunggu atau retry.
Commutative update: pilih increment dan decrement daripada menulis nilai absolut, sehingga compensation bisa diterapkan dalam urutan apa pun tanpa menimpa perubahan orang lain.
Reread before write: cek nilai terkini atau nomor versi tepat sebelum update, jangan mengandalkan apa yang dibaca saga di awal.
Two-phase commit vs saga: mana yang sebaiknya dipakai?
Tidak ada yang lebih baik secara umum. Keduanya berada di titik berbeda pada pertukaran antara isolation dan availability, dan perbandingan di bawah adalah titik awal saya.
Dimensi
Two-phase commit
Saga
Atomicity
Semua participant commit atau tidak sama sekali, diputuskan satu coordinator
Tiap langkah di-commit sendiri; keseluruhan atomic hanya secara eventual, lewat compensation
Isolation
Lock ditahan sampai ada keputusan, jadi state sebagian tetap tersembunyi
Tidak ada: state sebagian terlihat, jadi perlu semantic lock
Saat ada yang gagal
Coordinator crash membuat participant in-doubt terblokir sambil menahan lock
Langkah gagal memicu compensation; tidak ada lock yang ditahan antar langkah
Syarat participant
Setiap participant harus mendukung prepare, seperti PostgreSQL atau resource XA
Service apa pun dengan local transaction dan undo, termasuk HTTP API dan gateway
Paling cocok untuk
Beberapa database yang Anda operasikan sendiri, dengan transaksi singkat
Alur lintas service atau pihak ketiga dan proses bisnis yang berjalan lama
Biaya utama
Availability, ditambah prepared transaction yang harus dijaga seseorang
Usaha desain: compensation, idempotency, dan penanganan anomali
Jalankan pertanyaan berikut berurutan dan berhenti pada jawaban pertama yang menentukan.
Apakah setiap participant bisa prepare? Payment gateway lewat HTTP tidak bisa, dan itu langsung mengarah ke saga.
Bisakah satu database menampung semua datanya? Di satu VPS, satu transaksi Postgres mengalahkan kedua pattern, jadi tanyakan ini sebelum memilih salah satunya.
Bisakah Anda menulis undo untuk setiap langkah? Jika sebuah langkah tak punya compensation yang masuk akal, geser batas service alih-alih memaksakan saga.
Apakah bisnis bisa menerima pengguna lain sesaat melihat state sementara? Jika tidak, 2PC atau satu database adalah jawaban yang jujur.
Siapa yang akan memantau pekerjaan in-doubt? Baik pg_prepared_xacts untuk 2PC maupun log saga untuk saga butuh pemilik dan alert.
Mulailah dengan mencoba menghindari distributed transaction sama sekali. Jika tak terhindarkan, pilih two-phase commit hanya bila Anda mengendalikan semua participant dan bisa menerima blocking, dan pilih saga bila langkah melintasi service atau vendor dan Anda bisa menulis undo yang aman untuk tiap langkah. Apa pun pilihannya, pekerjaan belum selesai sampai ada alert ketika prepared transaction atau saga yang setengah jalan tertinggal.